5

There are zillion of ways to handle or ignore errors thrown from actions calls in Lightning controllers.

var action = cmp.get("c.controllerMethod");
action.setCallback(this, function(response) {
    if(cmp.isValid() && response.getState() === "SUCCESS") {

    }
    else {
         // <WHAT TO ADD HERE>
    }
});

$A.enqueueAction(action); 

If you search the web and SFSE you find all kind of solutions

  1. Simply not handling and displaying it
  2. Using console.log()
  3. Showing toast windows
  4. Writing the message to a that only displays on error
  5. ...

What I am looking for a best practise recommended by Salesforce.com and other experts. Important for me is:

  • Uses minimal code
  • Uses existing UI concepts from the Design System or even Base components
  • Works in all context and types of Lightning components
0

2 Answers 2

12

What I use is a component which handles those calls and turns errors into toasts:

ToastErrorHandler.cmp:

<aura:component >
    <aura:method name="handleResponse" action="{!c.handleResponse}">
        <aura:attribute name="response" type="Object" />
        <aura:attribute name="successHandler" type="Object" />
        <aura:attribute name="errorHandler" type="Object" />
    </aura:method>
</aura:component>

ToastErrorHandlerController.js

({
    handleResponse : function(component, event) {
        var params = event.getParam('arguments');
        var response = params.response;
        var successHandler = params.successHandler;
        var errorHandler = params.errorHandler;

        var state = response.getState();

        if(state == 'SUCCESS') {
            successHandler(response);
        } else {
            var toastEvent = $A.get("e.force:showToast");
            var message = '';

            if (state === "INCOMPLETE") {
                message = 'Server could not be reached. Check your internet connection.';
            } else if (state === "ERROR") {
                var errors = response.getError();
                if (errors) {
                    for(var i=0; i < errors.length; i++) {
                        for(var j=0; errors[i].pageErrors && j < errors[i].pageErrors.length; j++) {
                            message += (message.length > 0 ? '\n' : '') + errors[i].pageErrors[j].message;
                        }
                        if(errors[i].fieldErrors) {
                            for(var fieldError in errors[i].fieldErrors) {
                                var thisFieldError = errors[i].fieldErrors[fieldError];
                                for(var j=0; j < thisFieldError.length; j++) {
                                    message += (message.length > 0 ? '\n' : '') + thisFieldError[j].message;
                                }
                            }
                        }
                        if(errors[i].message) {
                            message += (message.length > 0 ? '\n' : '') + errors[i].message;
                        }
                    }
                } else {
                    message += (message.length > 0 ? '\n' : '') + 'Unknown error';
                }
            }

            toastEvent.setParams({
                title: 'Error',
                type: 'error',
                message: message
            });

            toastEvent.fire();
            if(errorHandler) {
                errorHandler(response);
            }
        }
    }
})

In a component where I want to use it, I just include the <c:ToastErrorHandler aura:id="toastErrorHandler" /> in the component. And then instead of directly writing a handler for the callback of an action, I call the method from ToastErrorHandler:

    var saveValuesAction = component.get("c.saveValues");       

    saveValuesAction.setParams({
        'formDescriptionId' : component.get('v.formDescriptionId'),
        'values' : JSON.stringify(values)
    });

    var toastErrorHandler = component.find('toastErrorHandler');

    saveValuesAction.setCallback(this, function(response){
        toastErrorHandler.handleResponse(
        response, 
        function(response){
            var toastEvent = $A.get("e.force:showToast");
            toastEvent.setParams({
                "title": $A.get("$Label.c.Form_Save_Success_Title"),
                "message": $A.get("$Label.c.Form_Save_Success_Message"),
                "type": "success"
            });
            toastEvent.fire();
        })
    });

    $A.enqueueAction(saveValuesAction);

This allows me to share consistent error handling around all of my server-calls. It's probably not the perfect solution yet, but I think something like this works pretty well.

7
  • That's really an elegant solution. You get the full score! You get extra credit if you help me get the ESLint/Lightning CLI error away. The repeated use of function(response) shows a "'response' is already defined in the upper scope" (eslint.org/docs/rules/no-shadow) Commented Jun 2, 2017 at 13:34
  • 1
    @Aidan - Does this meet: "Works in all context and types of Lightning components" since toasts do not work in lightning out.
    – Eric
    Commented Jun 2, 2017 at 20:20
  • 1
    @RobertSösemann I don't have an neat answer for that. I suppose you could create an intermediary variable "innerResponse" and pass that to the inner function. But the only reason for it would be to keep the linter quiet, so I don't like that very much. I don't use the linter (probably should!). If it supports an annotation to suppress the warning here, then that might be better.
    – Aidan
    Commented Jun 5, 2017 at 8:14
  • 1
    @Eric Lightning Out is a tricky one. The other UI-element I've used for error reporting is by using $A.createComponents() to create ui:message elements. It's very much like Visualforce. That would work in Lightning Out, but it could look weird inside LEX. If your component is just a small part of a record page, then toasts seem more appropriate. For Lightning Out, I guess you could have an outer wrapper which catches toast events and spits them out as ui:message elements?
    – Aidan
    Commented Jun 5, 2017 at 8:17
  • 1
    @RobertSösemann Sorry about that - It was actually a bug in the original code that I posted. I've updated that code. The change is that errors[i].pageErrors[j] should have been errors[i].pageErrors[j].message. I had only just written that component, eventually saw the problem myself, and never came back here to post the bugfix. Hopefully, that will do it for you.
    – Aidan
    Commented Aug 18, 2017 at 8:18
2

To be honest, this seems like more of a UX issue than a coding issue.

  1. Showing a toast

    I would typically use this if something essential to the component has failed, or if the success action would have been to show a toast, since the user would already be expecting to see something there.

  2. Printing an error message where the content should have been

    If the action that failed was not essential to the component, having a toast pop up can be too intrusive, and a failure message where content should have been would be an indicator to the user to try again.

  3. Print an error message to the console

    If the content on success is really not that essential, you might just want to leave that spot on the page blank rather than have an ugly error message there. I suppose you could just ignore the error, but a message in the console would make it easier to debug.

    This is particularly useful if apex is throwing an error due to data not existing or a lack of user permissions.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .