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:
- 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;- 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;- 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;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.
Akadenia APM Setup – see the
error-infra-setup folder.Written by

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.

