The full reliable error tracking in Next.js guide

Next.js provides a solid foundation for error handling, as outlined in their official documentation. However, in practice, it doesn’t always capture every type of error—particularly those thrown inside promises, React hooks, or event handler callbacks.
This limitation has been discussed in the Next.js GitHub issues #55462 and #53756. Although these issues have been closed, the underlying concerns remain relevant. As a result, we needed a more reliable way to catch all errors occurring in our app, including the ones that slip through Next.js’s built-in mechanisms.

This guide introduces a robust error-tracking setup you can integrate into your app to ensure that no runtime error goes unnoticed. We’ve tested this approach in real-world applications and found it effective—especially when working with the App Router and a mix of server and client components.

If Next.js later provides a built-in fix or improvement, we recommend adopting the official approach. But until then, this guide will save you time and provide a battle-tested solution. Depending on your setup, you can also pick and choose the parts that best fit your architecture.

Types of errors

There are mostly—but not limited to—three types of errors that can be thrown in your frontend app:

  1. UI crashing errors: These errors are thrown while rendering your component on the server side. They prevent the function from returning the DOM that represents your component. This type of error is caught by Next.js. Here’s an example:
// src/app/page.tsx

async function getData() {
  return await new Promise<string>((_, reject) =>
    setTimeout(() => {
      reject(new Error("Error getting data"));
    }, 1000)
  );
}

async function Home() {
  const data = await getData();
  return (
    <div className="flex flex-col items-center justify-center h-screen">
      {data}
    </div>
  );
}

export default Home;
Results in:
  1. Hook errors / uncaught promise rejections: These errors happen within your hooks—usually inside useEffect, for instance, when fetching data or performing animations. These are not caught by Next.js's error handler:
// src/app/page.tsx

"use client";

import { useEffect, useState } from "react";

function Home() {
  const [data, setData] = useState<string | null>(null);

  useEffect(() => {
    const getData = async () => {
      const data = await new Promise<string>((_, reject) =>
        setTimeout(() => {
          reject(new Error("Error getting data"));
        }, 1000)
      );
      setData(data);
    };
    getData();
  }, []);

  return (
    <div className="flex flex-col items-center justify-center h-screen">
      {data}
    </div>
  );
}

export default Home;
Results in:
  1. Event handler errors: These errors are thrown from processes triggered by user interactions. This is another type not caught by Next.js:
// src/app/page.tsx

"use client";

import { Button } from "@components/button";

function Home() {
  return (
    <div className="flex flex-col items-center justify-center h-screen">
      <Button
        onClick={() => {
          throw new Error("Error thrown from a button");
        }}
      >
        Click me to throw an error
      </Button>
    </div>
  );
}

export default Home;
Results in:

Solution

From our experience, it became clear that we needed a way to reliably capture the second and third types of errors that Next.js doesn’t catch by default. Once you’re able to capture these, you open the door to a range of possibilities—whether it’s reporting them to an APM like Sentry or Datadog, or displaying a custom toast notification for the user.

We’ll let Next.js handle the first type—UI crashing errors—using the built-in global-error.tsx file. Here’s an example of how we structure ours:

// src/app/global-error.tsx

"use client";

import { Button } from "@components/button";
import { useEffect } from "react";

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    console.error(error);
    // Add your error handling logic here
  }, [error]);

  return (
    <html lang="en">
      <body>
        {/* Your error UI */}
        <Button onClick={reset}>Reset</Button>
      </body>
    </html>
  );
}

Create an error tracer component

The ErrorTracer component is our magical client-side component that listens for global runtime errors and unhandled promise rejections. It uses the useEffect hook to add event listeners to the window object on mount and cleans them up on unmount to avoid memory leaks.

// src/components/error-tracer.tsx

"use client";

import { useEffect } from "react";

export default function ErrorTracer() {
  useEffect(() => {
    const handleError = (event: ErrorEvent) => {
      console.log("Caught error:", event.error?.message);
      // Add your logic here
    };

    const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
      console.log("Caught unhandled rejection:", event.reason?.message);
      // Add your logic here
    };

    window.addEventListener("error", handleError);
    window.addEventListener("unhandledrejection", handleUnhandledRejection);

    return () => {
      window.removeEventListener("error", handleError);
      window.removeEventListener("unhandledrejection", handleUnhandledRejection);
    };
  }, []);

  return null; // This component doesn't render anything
}

Hook the error tracer into the app layout

In layout.tsx, we use dynamic from Next.js to import ErrorTracer with { ssr: false }. This ensures it’s only loaded in the browser and avoids SSR issues when accessing window.

// src/app/layout.tsx

import "../styles/app.css";
import { PropsWithChildren } from "react";
import dynamic from "next/dynamic";

const ErrorTracer = dynamic(() => import("@components/error-tracer"), {
  ssr: false,
});

export default async function Layout({ children }: PropsWithChildren) {
  return (
    <html className="h-full" lang="en">
      <body className="min-h-full">
        <ErrorTracer />
        {children}
      </body>
    </html>
  );
}

Output

Here’s the output when testing with previous examples:

Conclusion

In summary, while Next.js provides a robust framework for error handling, it doesn't catch all types of errors—particularly those within promises, hooks, and event handlers. By implementing a custom error handling setup—including the ErrorTracer component and integrating it into your layout—you can effectively capture and manage these uncaught errors.

This approach ensures a more resilient application and enables better debugging and user experience through error reporting or toast notifications. Until Next.js improves its native handling, this guide gives you a production-ready fallback.

You’re welcome to check out our own repo for reference:
Akadenia APM Setup – see the error-infra-setup folder.

Written by

Engineering Team

Engineering Team

Development

Our engineering team is a group of highly skilled and experienced software engineers with a passion for building high-quality web and mobile applications. They are dedicated to creating reliable, scalable, and user-friendly software solutions that meet the needs of our clients.

5 from 2 votes

Tap a star to rate

More posts

The full NextJS Setup Guide For Error Reporting to Sentry

The full NextJS Setup Guide For Error Reporting to Sentry

The full guide for setting up Sentry for NextJS server side and browser side to report errors

Apr 10, 2025
The full NextJS Setup Guide For Error Reporting to Signoz

The full NextJS Setup Guide For Error Reporting to Signoz

The full guide for setting up SigNoz for NextJS server side and browser side to report errors

Apr 9, 2025