Error Handling
Catching render-time crashes with error.tsx, and showing custom 404s with not-found.tsx and notFound().
3 min de lectura
Errors in a Next.js app fall into two very different categories, and mixing up how you handle each one leads to either a crashed app or silently swallowed bugs.
Expected errors: return them, don't throw them
An expected error is a normal outcome of your app's logic — a failed API response, a validation failure, a resource that legitimately doesn't exist. These should be handled explicitly and shown to the user, not thrown as exceptions:
// app/page.tsx
export default async function Page() {
const res = await fetch("https://api.example.com/data");
if (!res.ok) {
return <p>Something went wrong loading this page. Please try again.</p>;
}
const data = await res.json();
return <p>{data.title}</p>;
}notFound() and not-found.tsx
When a specific resource doesn't exist — a blog post whose slug doesn't match anything — call notFound() from next/navigation. It stops rendering the current route and shows the nearest not-found.tsx instead:
// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) {
notFound();
}
return <article>{post.title}</article>;
}// app/blog/[slug]/not-found.tsx
export default function NotFound() {
return <div>404 — that post doesn't exist.</div>;
}Next.js correctly responds with an actual HTTP 404 status code here, which matters for SEO and for any tooling that checks response codes — this isn't just UI, the response itself reflects the right outcome.
Uncaught exceptions: error.tsx boundaries
An uncaught exception is a bug — something threw that your code didn't anticipate. Rather than crashing the entire app, an error.tsx file placed in a route segment catches errors thrown anywhere beneath it and renders a fallback instead:
// app/dashboard/error.tsx
"use client"; // error boundaries must be Client Components
import { useEffect } from "react";
export default function DashboardError({
error,
retry,
}: {
error: Error & { digest?: string };
retry: () => void;
}) {
useEffect(() => {
console.error(error); // send this to an error reporting service
}, [error]);
return (
<div>
<h2>Something went wrong.</h2>
<button onClick={() => retry()}>Try again</button>
</div>
);
}retry attempts to re-render the segment that crashed, which is often enough to recover from a transient failure (a flaky network request, for instance) without a full page reload. Note that error.tsx must be a Client Component — error boundaries are a React class-component mechanism under the hood, and the file-convention wrapper around it needs the client runtime to catch rendering errors as they happen in the browser.
Errors bubble up to the nearest boundary
If app/dashboard/settings/ has no error.tsx of its own, an error thrown there is caught by app/dashboard/error.tsx instead, and failing that, whatever error.tsx exists further up the tree. This lets you place a broad, generic fallback near the root and more specific, contextual ones deeper in the tree only where it's worth the extra effort.
global-error.tsx: the last resort
For errors thrown in the root layout itself — where no other error.tsx could possibly catch them, since there's no layout left above it — app/global-error.tsx is the final fallback. It's rare to need one, but when you do, it must define its own <html> and <body> tags, since it's standing in for the entire root layout when it activates.