Error Handling Best Practices for Lightning Web Components

Error handling is critical to any application and should be incorporated into an app right from the design phase. The use of well-defined error handling patterns and best practices ensures that your app handles both expected and unexpected errors consistently and gracefully. In this blog, we’ll go over some error handling best practices in Lightning Web Components. Since the Lightning Web Components framework is standards-based, most error handling best practices are based on standards as well.

Handling errors on the server side

Lightning Web Components is a UI framework that relies on the server for its data needs. The easiest way to work with Salesforce data using Lightning Web Components is to use base Lightning components and the Lightning Data Service wire adapter. But sometimes, you need to execute custom business logic and apply complex transformations before returning the data to the client, and this custom logic is typically written using Apex.

Apex methods can throw a variety of exceptions. These include Query exceptions, DML exceptions and general exceptions from business logic like parsing, type conversion, and so on. While you can let Apex code send unhandled exceptions directly to the client, handling errors on the server side allows you to control what kind of errors have to be surfaced to the client vs. what kind of errors have to be handled in the backend itself.

The best way to handle errors is to wrap the logic inside a try-catch block. However, there are some kinds of errors, like the limit exception (System.LimitException), that cannot be handled via a catch block. Whenever throwing exceptions to the front-end, it’s recommended that you create your own custom exception classes to customize the error message and abstract the exception details that are sent to the client.

try {
    // Perform logic that may throw an exception.
} catch (Exception e) {
    throw new CustomException(e.getMessage());
}

//Custom Exception class
public class CustomException extends Exception {

}

As a best practice, also decide on a single common structure to send errors and make sure to stick to it across all backend classes. With pre-defined error types, you can easily decide how much detail you want to return to the client.

Handling errors on the client side

Not all kinds of errors on the client side can be handled the same way. In this section, we’ll look at how the error handling mechanisms change with the origin of the error in Lightning Web Components.

The try-catch block

Though try-catch is the most common way to handle errors in code, it can be misused. Placing all of the code inside the try block is not considered a best practice. The code that is placed inside a try block must be based on how the error from that code is handled. If errors from different code blocks must be handled in different ways, use multiple try-catch blocks. Also, the catch block should only handle the errors that we expect to see in it, the rest should be propagated further.

It’s important to remember that the try-catch block can only handle exceptions in synchronous code. If an exception happens in asynchronous code execution, like in setTimeout or promises, then try-catch won’t catch it. In the example below, the catch block will not be executed because the error occurs within an asynchronous process.

try {
    setTimeout(() => {
        throw new Error('some error');
    }, 1000);
} catch (e) {
    console.error("An error occurred"); //This will not be executed
}

Errors from asynchronous operations like wire, imperative calls and promises

As mentioned earlier, errors from async calls cannot be caught by wrapping the call inside a try-catch block. In the case of timing events like setTimeout and setInterval, a try-catch block must be used inside the callback function to handle the errors.

setTimeout(() => {
    try {
        //logic
    } catch (e) {
        //handle error
    }
}, 300)

In the case of wired methods, there are two points of potential failure. These are during value provisioning by the wire adapter and during the execution of custom logic to handle the provisioned results. Whenever there are errors with value provisioning, they’re automatically stored in the error property. You can parse the property to know the cause of an error and handle it accordingly. If data is provisioned successfully, and you want to handle errors from the logic that handles data, place your logic inside a try-catch block as shown in the example below.

@wire(getContactList)
wiredContacts({ error, data }) {
    if (data) {
        try {
            //logic to handle result
        } catch(e){
            //error when handling result
        }
    } else if (error) {
        //error with value provisioning
    }
}

When using imperative calls or promises, you have two ways to handle the errors.

  1. Use a catch method to handle errors that are thrown in the entire promise chain. It includes errors from the server and errors from the logic that’s written in the then method. If you don’t use the catch() block, errors from the .then block will get swallowed. As a best practice, ensure all promises have a .catch statement.
getContactList()
    .then(result => {
        //logic to handle result... dont need a try catch
    })
    .catch(error => {
        //logic to handle errors
    });

In the example above, the catch() method handles errors thrown in both the getContactList and the then() block.

  1. Use the async/await pattern to call the asynchronous function and wrap the code inside a try-catch block just like you would a synchronous function.
async getDetails(){
    try{
        let result = await getContactList();
    } catch(e){
        // Handle Errors
    }
}

Errors during the component lifecycle

Component lifecycle includes initialization of the class and its lifecycle hooks like constructor(), connectedCallback() and renderedCallback(). While the logic inside the lifecycle hooks can be wrapped inside a try-catch block, errors when computing property values can’t be handled the same way, as shown in the example below.

export default class PropertyInitErrorExample extends LightningElement {
    sum = 10;
    count = 0;
    avg = sum/count; //results in an exception
}

Try to avoid computing values for class fields/properties inline, and use getter methods for computation so that the logic inside the method can be wrapped in a try-catch block if needed.

You can also use a boundary component with the errorCallback() hook to handle errors during the component lifecycle. We discuss this further in the Error Lifecycle and Propagation section.

Displaying and logging errors

Now that we’ve seen how to catch different kinds of errors, let’s look at some best practices on displaying these errors in Lightning Web Components.

Understanding the error body payload

Before displaying an error, it’s helpful to understand the composition of the error object that you catch in various scenarios.

JavaScript and Web Platform APIs throw or reject error types like ReferenceError, TypeError and so on. These all inherit from the Error object which has the following properties:

  • name – Indicates the type of exception thrown.
  • message – Contains the error message.
  • stack – Contains the stack trace.

Here is an example code snippet and the resulting console output.

try{
    undefinedVariable.toString();
} catch(e){
    console.error(e);
    console.error('e.name => ' + e.name );
    console.error('e.message => ' + e.message );
    console.error('e.stack => ' + e.stack );
}

 

 

 

 

 

 

 

 

 

 

 

 

 

When accessing Salesforce data using Apex or Lightning Data Service, errors are presented using a custom error object with a slightly different structure modeled after the Fetch API’s Response object. Here are a few properties of the custom error object:

  • ok – Specifies whether the request was successful or not.
  • status – Contains the HTTP status code of the response. For example, 500 for an internal server error.
  • statusText – Contains the status message corresponding to the status code.
  • body – The response body which varies based on the method that throws the error.

For unhandled exceptions and custom exceptions thrown by Apex, the body property holds additional details like the type of exception (body.exceptionType), the error message (body.message), the Apex stack trace (body.stackTrace) , and others. Here’s an example code snippet and the resulting console output.

//Apex Code
@AuraEnabled
public static Integer someMethod() {
    return 10/0;
}

//JavaScript Code
someMethod()
    .then(result => {
        //Handle Result
    })
    .catch(error => {
        console.error(error);
    });

Errors occur in Lightning Data Service when a resource, such as a record or an object, is inaccessible on the server or if you pass in an invalid input to the wire adapter (such as an invalid record Id or missing required fields) or if validation rules fail. Lightning Data Service returns a custom error object that’s very similar to what Apex returns, but the body property of the error depends on the API that returns it. It may contain a single object or an array of objects, so the logic must check for both data types when parsing it. Here’s an example:

@wire(getRecord, { recordId: '$recordId', fields })
wiredRecord({error, data}) {
    if (error) {
        // UI API read operations return an array of objects
        if (Array.isArray(error.body)) {
            this.error = error.body.map(e => e.message).join(', ');
        } 
        // UI API write operations, Apex read and write operations 
        // and network errors return a single object
        else if (typeof error.body.message === 'string') {
            this.error = error.body.message;
        }
    } else if (data) {
        // Process record data
    }
}

As you have seen, the structure of the error object is different in each case. Instead of repeating the logic to parse different kinds of error objects in each component, you can create a single function that does this and import it as a module in each component.

As a best practice, use the reduceErrors function from LWC Recipes (sample app which has a collection of easy-to-digest code examples for Lightning Web Components) to handle different kinds of error objects. This function looks for the message property and concatenates the message if multiple message properties are found. Here’s an example of how it can be used to simplify the previous code snippet.

import { reduceErrors } from 'c/utils';
...

@wire(getRecord, { recordId: '$recordId', fields })
wiredRecord({error, data}) {
    if (error) {
        this.errorMessage = reduceErrors(this.error);
    } else if (data) {
        // Process record data
    }
}

However, it’s important to remember that the error body payload might be slightly different when dealing with third party code. The throw statement in JavaScript can throw any expression, including a number or string. Therefore, you need to be careful when handling exceptions from third party code, as the approach discussed above might not suffice.

Displaying errors

This is perhaps the most important part of any error handling mechanism. Errors have to be displayed in meaningful ways to the user. The most recommended way is to show the error to the user near the point of failure. For example, a text field. A toast message is more appropriate if the error occurs when a button is clicked, or if the error is related to multiple points of failure.

Base Lightning Components provide an easy and consistent way to show error messages in forms. Base Lighting Components automatically add and remove CSS classes to form controls and form itself depending on the current validation state. The reportValidity and setCustomValidity methods can be used to programmatically control error messages.

Error messages also have to be user-friendly. Only showing that an error has occurred is not helpful. The message should indicate exactly what the error is, and what the user can do to correct it.

To have a consistent error handling and display mechanisms, it’s a best practice to create a reusable component to display errors, and use it in all components. This is what we did with the errorPanel component in the LWC Recipes sample app. This component also uses the reduceErrors function that we discussed earlier to handle all formats of error objects and show a consistent user interface. Here’s an example:

<template if:true={error}>
    <c-error-panel errors={error}></c-error-panel>
</template>

Logging errors

Other than displaying errors to the user, you can also log them to the console. As a best practice, use the console.error() function as it preserves the call stack in the original error message and also captures the call stack from where the error message is logged. This can be displayed by clicking on the arrow next to the message in the DevTools console.
All the console logging APIs accept multiple arguments. If more information needs to be added to the caught error, use console.error('Unexpected error during some operation', error);

Also, wherever possible, errors need to be logged to the server for better tracking and reporting purposes. This will prove most useful when debugging issues on production. Once the application is in production, console.error by itself adds no value because the logged output cannot be accessed by the developer unless explicitly shared by the end user.

Error lifecycle and propagation

Not all errors can be displayed where they originate and not all errors occur in components that have a user interface (e.g. service component). Such errors must be propagated to a parent component to be displayed. Unhandled errors are propagated by default through the component hierarchy.

Lightning Web Components error lifecycle

When an error occurs in code, Javascript looks for a handler that catches the error. It’s a best practice to handle the error as close as possible to the origin of the error. In the case of Lightning Web Components, unhandled errors propagate from child components to parent components. If the topmost parent component doesn’t handle the error, it’s thrown to the Lightning runtime.

Errors from synchronous operations are handled by the Lighting runtime by showing the “Sorry to interrupt” popup with the line number and stack trace of the error — the error is not further propagated to the browser. In the case of errors from async operations like wire functions, promises, and so on, the error propagates to the browser and shows up in the browser’s console. The runtime also shows the error on the screen depending on the context. For example, when running a Lightning Web Component in Lightning Experience, async errors aren’t shown in the UI. But, when running a Lightning Web Component inside a flow, the flow runtime shows the error at the bottom of the screen.

 

Propagating errors

As we saw in the lifecycle above, unhandled errors propagate by default. But when using any of the error handling mechanisms above, you can choose to handle errors at the component level or manually propagate it further for other components to handle. Errors can be propagated either by using the throw keyword or by using custom events. The throw keyword halts the function at the point where it’s used, while custom events give you the flexibility to decide what happens after you fire the event. Errors thrown using the throw keyword can only be handled by a component’s parents, but using custom events gives you the flexibility to handle them using components outside the hierarchy as well.

As a best practice, propagate errors from lower level components (e.g., service components, utility functions) and handle errors in higher level components. The reason exceptions are handled at higher levels is because the lower levels don’t know what the most appropriate course of action is to handle the error.

Here’s an example that shows two variations of the same function, one that throws an error and one that fires a custom event.

export default class Hello extends LightningElement {

    //Using Throw keyword
    divide_with_throw(a, b){
        if(b == 0){
            throw new Error('Cannot divide by 0'); 
        }
        return a/b;
    }

    //Using Custom Events. You can also use Pubsub or Lightning Message Service.
    divide_with_event(a, b){
        if(b == 0){
            const selectedEvent = new CustomEvent('error', { detail:'Cannot divide by 0' });
            this.dispatchEvent(selectedEvent);
        } else {
            return a/b;
        }
    }
}

The next step would be to handle these errors in a parent component.

If you use custom events, you can just write event handlers for those events. It’s important to note that custom events aren’t actual errors, so they cant be “caught“ using catch blocks.
If you throw custom errors, you can either catch individual errors using the error handling mechanisms mentioned above, or use the errorCallback() hook to capture all the unhandled and custom errors.

errorCallback() is a lifecycle hook that captures errors from all the descendent components in its tree. It captures errors that occur in the descendant’s lifecycle hooks or within event handlers declared in the component’s HTML template. There are a few things to remember about the errorCallback() hook – It only catches errors in the handlers assigned via the template. Any programmatically assigned event handlers won’t be caught. Once an error is caught, the framework unmounts the child component that threw the error from the DOM. It catches errors that occurs in the descendant components but not itself.

As a best practice, create a boundary component that implements the errorCallback() and embed your functional component inside it. Here is an example of a boundary component where the errors are caught using the errorCallback() hook, and displayed using the errorPanel component (which in turn uses the reduceErrors function as discussed earlier).

<template>
    <template if:true={this.error}>
        <c-error-panel errors={this.error}></c-error-panel>
    </template>
    <template if:false={this.error}>
        <!-- YOUR COMPONENT --> 
    </template>
</template>
import { LightningElement } from 'lwc';

export default class Boundary extends LightningElement {
    errorCallback(error, stack) {
        this.error = error;
    }
}

Finding the right balance

It’s important to remember that not all the errors need to be caught and handled in the component (e.g. errors in lower level components). Sometimes it’s also easier to identify and fix the root cause of an error if it’s left uncaught. This is particularly useful during the development and testing phase. However, it can be hard to find the right balance between when to catch errors and when not to catch errors, so here are a few tips:

  • Letting the application fail is always preferable to poorly handling the errors.
  • Make sure that you always gracefully handle errors when dealing with external or third party code.
  • Make sure that you always handle errors on application boundary points like calls to the server, third party libraries, and external services.
  • Don’t be afraid to throw errors in your own code when needed.

Summary

In this blog, we’ve seen the different ways to handle errors depending on which part of the code they occur in. We’ve also seen different formats of the error object and how the reduceErrors function can help you extract error messages; and how the creation of a boundary component can help safeguard your component tree from unhandled errors. Lastly, we reviewed how to propagate error messages up the component hierarchy using events and throw statements. You can explore the apps in our Sample Gallery to see best practices in action.

For further learning, here are a few extra resources:

 

About the Author

Aditya Naag Topalli is a 13x Certified Lead Developer Evangelist at Salesforce. He focuses on Lightning Web Components, Einstein Platform Services, and integrations. He writes technical content and speaks frequently at webinars and conferences around the world. Follow him on Twitter @adityanaag.