DEV Community

Cover image for How to Handle Errors in Next.js for Node With the App Router
Antonello Zanini for AppSignal

Posted on • Originally published at blog.appsignal.com

How to Handle Errors in Next.js for Node With the App Router

Error handling in Next.js is critical to providing a seamless experience to your users even when things go wrong. Without proper error management, users may get confused about what has happened and even leave your site. To avoid that, you must ensure that they receive informative feedback about errors and provide a way to recover from them.

In this article, you'll see:

  • Why the default Next.js error handling logic falls short
  • How the error.js file convention allows you to handle errors in the App Router
  • How to define custom logic to deal with both client-side and server-side errors in Next.js

Let's jump right in!

Default Error Handling in Next.js

When a server error occurs in a Next.js application, the following '500 Internal server error' page is returned by default:
Server error

Instead, this view is loaded when a fatal client-side error occurs:
Client-side error

There are two huge issues with this behavior:

  • Generic error messages: Both the "Internal server error" and "Application error: a client-side exception has occurred (see the browser console for more information)" messages are too generic for you to understand what happened.
  • No recovery: Users don't have the opportunity to recover from the error and are forced to manually reload the page or leave.

As a result, the default Next.js error handling system leads to a pretty bad user experience. Here's why you need to override that with custom error handling logic!

How to Handle Errors in Next.js Using the App Router

Error handling in a Next.js application with the App Router revolves around the error.js file convention.
If you aren't familiar with that, error.js is an optional file inside a route segment that exports a UI React component to handle errors gracefully.

To better understand how this mechanism works, let's analyze two possible scenarios:

  • Local and nested errors
  • Root-level errors

Local Error Handling

Assume that you defined an error.js file in a route segment:
Route segment

When generating the /dashboard page, the App Router will automatically create a React Error Boundary and wrap the page.js component as follows:
React error boundary

Now, consider if an error occurs in page.js or any of its nested child route segments. That can be either a client-side or server-side error (as error.js can deal with any type of error).

The Error Boundary will intercept the error and render the React component exported by error.js as a fallback. In particular, the error component will be rendered within the surrounding layout. This means the error component won't take the entire view as it does in the default Next.js error handling logic, but occupy only a portion of the page. That leaves room for recovery, as the layout will maintain its state and remain interactive.

That's why the component exported by an error.js file accepts two props:

  • error: Contains the Error instance with the details of the error that occurred in the client or the server. In production, server-side errors include only generic message and digest properties to avoid leaking sensitive information.
  • reset: A function to attempt to recover from the error. When called, the function tries to re-render the original component nested in the Error Boundary. If successful, the fallback error component is replaced with the newly rendered result. Otherwise, the fallback error component is left on the page.

This error handling mechanism works on a per-route-segment basis, with errors bubbling up to the nearest error boundary. By placing error.js files at different levels in the nested hierarchy, you can then achieve more or less granular error UI management.

Root Error Handling

error.js boundaries can't catch errors occurring in layout.js or template.js components of the same segment. Instead, those errors will be intercepted by the error.js file in the parent segment.

But what happens if an error is thrown in the root app/layout.js or app/template.js component? Since the root app/error.js boundary won't be able to catch it, Next.js requires a special component. To specifically handle errors in the root layout.js and template.js components, you must place a global-error.js file in the root app directory. That is nothing more than a variation of error.js, exporting a component with the same props.

The main difference with error.js is that the global-error.js error boundary wraps the entire application. So its fallback component will replace the entire view and should always contain the <html> and <body> tags.

global-error.js is the least granular UI component and can be considered the last resort for error management. Although it's essential to ensure complete Next.js error handling, it's unlikely to get rendered often, as the root layout and template components are generally static and less prone to errors.

Implement Error Handling in Next.js

Follow the instructions below to handle errors in Next.js with error.js files.

Prerequisites

To go through this tutorial, you need Node.js 18.17 or later installed on your machine. In particular, the code snippets below will refer to a Next.js 13+ application with the App Router.

For access to the entire code of the project you're about to build, clone the GitHub repository that supports this article:

git clone https://github.com/Tonel/custom-error-handling-nextjs
Enter fullscreen mode Exit fullscreen mode

To test the application, enter the project folder and install the local dependencies:

cd custom-error-handling-nextjs
npm install
Enter fullscreen mode Exit fullscreen mode

Then, build the Next.js application:

npm run build
Enter fullscreen mode Exit fullscreen mode

This will take a while, so be patient.

Once it finishes, you can launch the application with:

npm run start
Enter fullscreen mode Exit fullscreen mode

The demo Next.js application with custom error handling logic should now be running at http://localhost:3000.

If you prefer to start from scratch, launch the create-next-app command and follow the instructions to set up a new Next.js application.
Refer to the GitHub repository for the complete code, as the sub-chapters below will cover only the main steps of implementing error handling in Next.js.

Create An error.js File

Add an error.js file to the root app directory and initialize it as below:

// ./src/app/error.js

"use client";

import styles from "./error.module.css";

export default function Error({ error, reset }) {
  // custom logic (e.g., log the error or send it to an APM service)

  return (
    <div className={styles.error}>
      <div className={styles.oops}>Oops!</div>
      <div className={styles.message}>Something went wrong...</div>
      <div>
        <button className={styles.retryButton} onClick={() => reset()}>
          🔄 Retry!
        </button>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

error.module.css is a local CSS module containing the styling for the fallback error component. Inside these error UI components, you can also add custom logic, such as logging and application monitoring integrations. In a real-world scenario, you should provide a meaningful explanation for the error.

Keep in mind that error components must be client components. If you forget to add the "use client" instruction, Next.js will raise this error:

ReactServerComponentsError:
./src/app/error.js must be a Client Component. Add the "use client" directive to the top of the file to resolve this issue.
Learn more: https://nextjs.org/docs/getting-started/react-essentials#client-components
Enter fullscreen mode Exit fullscreen mode

This is what the <Error /> component returned by error.js looks like:
Error component

As you can see, it presents the error gracefully to the user and allows them to recover through the "Retry" button.

Create a global-error.js File

Create a global-error.js file in the app directory and add the following lines to it:

// ./src/app/global-error.js

"use client";

import Link from "next/link";

export default function GlobalError({ error, reset }) {
  // custom logic (e.g., log the error or send it to an APM service)

  return (
    <html>
      <body>
        <div className={"globalErrorContainer"}>
          <div className={"globalErrorDiv"}>
            <div className={"globalError500"}>
              <h1>500</h1>
              <h2>Internal Error</h2>
            </div>
            <Link className={"home"} href={"/"}>
              Home Page
            </Link>
          </div>
        </div>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

If the application crashes due to a fatal error in the root layout or template files, a reset() call is unlikely to resolve the situation.
As a recovery strategy, you should instead add a link to the home page or force a page reload with window.location.reload();.

Note that Next.js will ignore any CSS module or global file imports in global-error.js.
To properly style this component, add the required CSS classes to your global root CSS file.

Here's how the <GlobalError /> component appears:
Global error component
In a real-world site, replace "Internal Error" with a better error message.

Bear in mind that Next.js ignores the global-error.js file in development mode.
In that scenario, a fatal error in the root layout or template component triggers an overview with detailed debugging information, like this:
Fatal error

To test the error component returned by global-error.js, you must first build your application and then run it with npm run start.

Get Ready to Trigger Errors

To verify that error.js and global-error.js work as expected, define a <ErrorButton /> component:

// ./src/app/components/ErrorButton.js

"use client";

import { useState } from "react";
import styles from "./error-button.module.css";

export default function ErrorButton(props) {
  const [raiseError, setRaiseError] = useState(false);

  if (raiseError) {
    // "a" is undefined so "props.a.b" will result in an error
    return props.a.b;
  } else {
    return (
      <button
        className={styles.errorButton}
        onClick={() => setRaiseError((error) => !error)}
      >
        {props.label}
      </button>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

When clicked, raiseError becomes true and a client-side error is thrown.

Now, nest that component in layout.js to trigger global errors and in page.js to trigger local errors.
Here's what the home page will contain:
Home page

Wonderful! You're now ready to test the custom Next.js error handling logic!

Put It All Together

Launch the complete Next.js application as described in the Prerequisites section of this post.

Open http://localhost:3000 in the browser and click the "Simulate Local Error" button:
Local error

The <Error /> component will be rendered in the global layout UI as desired.
By clicking "Retry," the App Router will re-render the page.js component from scratch.
This means that the raiseError state property from <ErrorButton /> will be assigned to false and the error will be recovered.

Now, click the "Simulate Global Error" button:
Global error button

This time, the <GlobalError /> component will replace the entire view as expected.

Et voilà! You are now a Next.js error handling master!

Error Handling in Next.js: Best Practices

Handling errors in Next.js can be challenging, especially if you don't follow these best practices:

  • Always define a global-error.js file: Even though the error.js boundaries will catch most errors, your project must have a global-error.js file to make sure that even fatal errors at the root layer are handled gracefully.
  • Don't use global-error.js as a replacement for error.js: Even if your project has global-error.js in place, you should define a root error.js to handle non-root errors within the global layout.
  • Add recovery logic: Your users should always have the opportunity to recover from the error situation.
  • Avoid leaking implementation details: The error message should be detailed enough to explain what happened to the user. At the same time, it shouldn't provide implementation or sensitive information to potential attackers.

Wrapping Up: Next.js Error Handling Made Easier!

In this blog post, you saw how to introduce custom error handling logic to your Next.js application using the App Router, including:

  • How Next.js handles errors by default
  • How to define error UI components in the App Router
  • The best practices for secure and effective Next.js error handling

Thanks for reading, and see you in the next one!

P.S. If you liked this post, subscribe to our JavaScript Sorcery list for a monthly deep dive into more magical JavaScript tips and tricks.

P.P.S. If you need an APM for your Node.js app, go and check out the AppSignal APM for Node.js.

Top comments (0)