HomeReadLeveraging Error And Not Found Boundaries In Next Js

Leveraging error and not-found boundaries in Next.js

Published Jan 03, 2025
3 minutes read

I’ve seen applications break on errors and completely render it useless. Luckily Next.js gives us some neat tooling to handle these gracefully and even better now with error boundaries however, there is one thing that needs improvement. We lack proper feedback as to why something went wrong in a production environment.

With the architectural move towards the app directory and react server components in Next 13 we are able to perform some nifty tricks. With that change came some new reserved file names to catch our errors such as error.tsx and not-found.tsx. Since i've had to migrate some projects to the new app directory I wanted to share my learnings.

New directory structure

With the app directory, your structure would look something like this.

/app
  ├── layout.tsx
  ├── page.tsx
  ├── global-error.tsx // uncommon error fallback
  ├── not-found.tsx // root not-found handles any unmatched URLs for your whole application
  ├── /dashboard
      ├── page.tsx
      ├── error.tsx // granular error, if a fetch in page.tsx throws
      ├── not-found.tsx
  ├── /post/[slug]
      ├── page.tsx
      ├── not-found.tsx // invoke notFound() if in page.tsx !post

The UI could look like this to help a little with the visualization of it all.

Differences between handling errors or 404 in the pages vs app directory

In Next.js, error handling differs between the pages and app directories due to differences in routing and rendering paradigms. Here are some key differences.

Featurepages directoryapp directory
Handling 404sreturn { notFound: true } in getServerSideProps or next/errornotFound() function
Custom 404 Pagepages/404.jsapp/not-found.tsx
Custom Error Handlingnext/error or throwing JavaScript errorserror.tsx with automatic error boundaries
Client vs Server ComponentsMostly client-renderedMix of server and client components

When to show a not-found.tsx?

The notFound() function throws a NEXT_NOT_FOUND error under the hood. In general we invoke the notFound() function for the client or server when…

  • The requested resource does not exist
  • The user navigates to a valid route that points to a missing entity (like a product, user, or blog post).

Example on a post page.

import { notFound } from 'next/navigation';
 
export default async function PostPage({ slug }) {
  const post = await fetchPost(slug)
 
  if (!post) {
    notFound()
  }
 
  // ...
}

When to show an error.tsx?

We use the error.tsx to catch for unexpected errors and unhandled exceptions. These might include…

  • Network errors
  • API errors due to server issues like 500’s etc
  • Any other unexpected errors

The way we propagate the error from the fetch on a page.tsx to the error.tsx is by throwing an error.

const fetchData = async () => {
  try {
    const res = await fetch(SOME_API);
    return res;
  } catch (error) {
    throw error; // Re-throw here
  }
};
 
export default async function Page() {
  const data = await fetchData();
 
  // ...
};

Notice how there is no try/catch block on the page. This is because Next.js handles the throw in fetchData() and propagates it to the error boundary by having included the error.tsx file.

The absence of meaningful error messages in production

If you’ve messed around and thrown some errors around in a development environment, you are able to log the actual server side errors. However, in a production environment, the error is serialized by react and any custom error message you pass in the error is replaced with the following.

Error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details.

A digest property is included on this error instance which may provide additional details about the nature of the error. But it does not overwrite the digest which allows us to pass anything we want.

I ran into a case where I needed to give the user feedback and developer meaningful feedback and maybe I wanted to do something based on an error code that was returned by the backend. There’s an open discussion about this on the Next.js repo.

Passing a custom error code

The solution to this is to create a new custom Error object and use the digest property to pass any data you want. In my case i’d pass an error code that I can use to show a localised message.

const fetchData = async () => {
  try {
    const res = await fetch(SOME_API);
    return res;
  } catch (error) {
    const customError = new Error(error.message || 'custom message');
    (customError as any).digest = "CUSTOM_ERROR_CODE";
    throw customError; // re-throwing here
  }
};

Then in our error.tsx file we can read the code and perform an action.

'use client' // Error components must be Client Components
 
import { useEffect } from 'react'
 
enum ErrorId {
  CUSTOM_ERROR_CODE = 'CUSTOM_ERROR_CODE',
  INVALID_ID = 'INVALID_ID',
}
 
export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string | ErrorId }
  reset: () => void
}) {
  useEffect(() => {
    // Log the error code to an error reporting service
    console.error(error.digest)
  }, [error])
 
  return (
    <div>
      <h2>{getErrorMessage(error.digest as ErrorId)}</h2>
      <button
        onClick={
          // Attempt to recover by trying to re-render the segment
          () => reset()
        }
      >
        Try again
      </button>
    </div>
  )
}
 
// We can localize these messages now.
function getErrorMessage(code?: ErrorId) {
  switch (code) {
    case ErrorId.CUSTOM_ERROR_CODE:
      return 'This is a custom error code.';
    case ErrorId.INVALID_ID:
      return 'The ID used is invalid.';
    default:
      return 'Oops, something went wrong.';
  }
};
 

These examples are ofcourse contrived but should provide the foundation to be able to debug and handle errors gracefully in your Next.js application. By leveraging error boundaries and the digest property, you can create a robust error handling system that provides meaningful feedback to both users and developers, while maintaining security in production environments. The digest property has imo. never had any value for me in the past.