React Logging and Error Handling Best Practices

on October 20, 2020

Logging is essential for understanding what’s happening within your running code. Furthermore, you can use log data to track various metrics such as HTTP error rates, failed login attempts, and overall application availability. That’s not all, though. Log aggregation platforms allow for log analysis and the creation of advanced alerting mechanisms to detect anomalies. For example, a high number of failed login attempts might indicate someone tried to gain illicit access to your application.

Often, React developers choose to use the console.log statement to log important data, such as an error stack trace, to the developer console. However, this isn’t advised because whenever you refresh the webpage, you can’t access the logged data anymore. For this reason, we need a better way to log important events.

This article provides you with best practices for handling errors in React and logging them persistently. But first, it’s important for you to understand the close relationship between the concepts of error handling and logging. The connection between these concepts applies to all programming languages and frameworks, not just React.

What’s the Synergy Between Error Handling and Logging?

So, how are the concepts of error handling and logging closely related? To answer this question, let’s define both concepts.

Error Handling

First, let’s discuss error handling (also called exception handling). Exception handling refers to catching errors and handling them gracefully without impacting the user experience; this might include showing a blank page or error message, for example. As Charles1303 describes in his dev.to article, “Exception handling can be seen as a way of providing Resilience to our application.” In other words, if you handle exceptions, your application won’t crash when it throws an exception. You ensure your application can continue to operate normally with little impact on the user experience.

Logging

In contrast, logging is a means of auditing to keep track of things happening within an application. You should log important events, such as the completion of user requests. For React specifically, you can log React state updates to audit how your global state changes.

In addition, logging has proven useful when you want to handle exceptions. It’s important to log exceptions so your monitoring tool can alert you when something goes wrong. Furthermore, logging error messages associated with an exception can help you find the root cause of an issue much faster. An error message often contains many details about where the issue popped up and what went wrong. It’s an efficient and automated way to detect new exceptions and receive context for the raised exception.

In short, logging is crucial for your monitoring tool to track various metrics, such as error rates. You don’t want to miss out on identifying exceptions, especially if they negatively affect user experience.

React Error Handling Best Practices

We’ll explore two different approaches to error handling in React. The first is the React error boundary component and the second is the try-catch statement.

1. React Error Boundaries and Logging

Recently, React v16.0 introduced error boundaries. Error boundaries are React components designed to catch exceptions anywhere in a child component tree, log those errors, and display a fallback user interface (UI) of the component tree that crashed. Without error boundaries, exceptions can cascade throughout your application and crash your React app. They act as a fence around a component to catch any exceptions it throws and minimize their effects. In other words, they solve the problem of exceptions falling through and causing your React application to crash.

Imagine the following simple application component layout. We have the standard <App> component, which encapsulates both our <Header /> and <Main /> components.

<App>
    <Header />
    <Main />
</App>

We don’t expect any problems with the <Header /> component, but the <Main /> component can cause trouble. What if this component raises an exception? The application crashes if we don’t catch it.

To avoid this, we use the error boundary component, which encapsulates the <Main /> component:

<App>
   <Header />
   <ErrorBoundary>
      <Main />
   </ErrorBoundary>
</App>

By adding this fence around the <Main /> component, an exception can’t spill out and cause other components or the whole application to crash.

How Do You Define an Error Boundary Component?

In the React Error Boundary documentation, you’ll find the following information: “A class component becomes an error boundary if it defines either (or both) of these lifecycle methods static getDerivedStateFromError() or componentDidCatch().”

The static getDerivedStateFromError() function allows you to update the React state so the next render shows the fallback UI. For this, you can use a simple hasError Boolean to trigger the fallback UI.

The componentDidCatch() function serves as the entry point for logging error data. Here, you can decide to send logs to an external log management platform such as SolarWinds® Loggly®. Here’s an example of how you can use the componentDidCatch() function:[TA(1] 

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state to trigger fallback UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // Log error or send logging data to log management tool
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // Render fallback UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

Lastly, you can define a render() function that allows you to show a fallback UI. To keep things simple, you can display a small notification to the user, as in the example above.

2. Old-School Try-Catch Statement

There’s nothing wrong with using a try-catch or try-catch-finally statement to log data and handle errors. Both solutions allow you to catch and handle exceptions. If you have doubts about a piece of code, you can opt for a try-catch statement. However, a try-catch block doesn’t catch exceptions coming from child components, which means exceptions coming from child components can still cascade through your other components. Therefore, using a try-catch statement isn’t considered a best practice when it comes to handling component-level exceptions in React.

However, the try-catch statement is a best practice and is useful for catching event handler exceptions in React. Because event boundaries don’t catch errors inside React event handlers—such as an onClick event—you should use a try-catch statement to handle these exceptions. Take a look at the below example. The handleClick function takes care of the onClick event handler. However, we need a try-catch block to handle any exceptions thrown by these components.

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    try {
      // Do something that could throw
    } catch (error) {
      this.setState({ error });
      // log error or send to log aggregation tool
    }
  }

  render() {
    if (this.state.error) {
      return <h1>Caught an error.</h1>
    }
    return <button onClick={this.handleClick}>Click Me</button>
  }
}

As you can see in the snippet of code above, we store the error object in our state. Next, the render function checks if there’s an error. If yes, we render a fallback UI.

Thus, the try-catch block still has a purpose within React programming. You can use the try-catch block to log errors and transport error data to a log aggregation platform.

Summary

To summarize, your best option for handling errors in React is to use React’s error boundary component. Error boundaries work just like a try-catch block but for components. There are three main reasons you should use error boundaries:

  1. They can act as a fence around components to catch and handle JavaScript exceptions without unmounting the whole React component tree
  2. They can log errors (you can choose to log errors to the console or send them to a log management platform—such as the Loggly logging platform—for further analysis)
  3. They can render a fallback UI to limit the negative impact on user experience

If you prefer not to use error boundaries, make sure you still add at least one error boundary at the root of your app. This lets you render a fallback UI instead of a blank HTML page in case of an unhandled exception. However, remember error boundaries don’t work for event handlers. Instead, we recommend you rely on a try-catch block to catch any exceptions in event handlers.

Lastly, you can use the try-catch block or error boundary component in React to log errors and transport them to a log management platform. You can use your error data to track related React metrics and resolve issues faster. Additionally, you can use a console.log statement to log React error data to a user’s developer console; however, whenever they refresh the page, they’ll lose all their error data.

Want to learn more? Check out this post about best practices for client-side logging and error handling in React.

This post was written by Michiel Mulders. Michiel is a passionate blockchain developer who loves writing technical content. He also loves learning about marketing, UX psychology, and entrepreneurship. When he’s not writing, he’s probably enjoying a Belgian beer.

Related Posts