Handling Errors in Next.js App Router
No application is perfect—pages crash, APIs fail, databases go down. The App Router's error.tsx file creates React error boundaries that catch exceptions in child routes and display a custom error UI instead of a blank white screen. This article covers building resilient error boundaries, logging errors, and recovering gracefully so users always see a helpful message.
What is error.tsx?
error.tsx is a special file that defines a component wrapping all child routes in a folder with an error boundary. When a page component throws an error, error.tsx catches it and renders a custom UI. Instead of crashing your app, users see a message like "Something went wrong—try again" with a recovery button.
A typical error boundary:
// app/error.tsx
"use client";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h2 className="text-2xl font-bold text-red-600 mb-2">Oops!</h2>
<p className="text-gray-600 mb-6">Something went wrong.</p>
<button
onClick={reset}
className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700"
>
Try Again
</button>
</div>
</div>
);
}
The error prop contains the error object; reset is a function that re-renders the page and clears the error state.
File Structure and Scope
Each folder can have its own error.tsx. The error boundary wraps all child routes in that scope.
app/
├── error.tsx # Root error boundary (catches errors in all routes)
├── page.tsx
├── blog/
│ ├── error.tsx # Catches errors in /blog/* routes
│ ├── page.tsx # /blog
│ └── [slug]/
│ ├── page.tsx # /blog/:slug
│ └── error.tsx # Catches errors in /blog/:slug only
When an error happens in /blog/:slug, the nearest error.tsx (at app/blog/[slug]/) catches it. If there's no error.tsx at that level, the parent app/blog/error.tsx catches it. If no error boundary exists in the entire tree, Next.js shows a default error page.
Building an Error Boundary
Step 1: Root Error Boundary
Create a root-level error handler for unexpected crashes:
// app/error.tsx
"use client";
import { useEffect } from "react";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
useEffect(() => {
// Log the error to an external service (e.g., Sentry)
console.error("App error:", error.message);
}, [error]);
return (
<html>
<body>
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
<div className="bg-white p-8 rounded-lg shadow-lg text-center max-w-md">
<h1 className="text-4xl font-bold text-red-600 mb-2">500</h1>
<h2 className="text-2xl font-semibold mb-2">Application Error</h2>
<p className="text-gray-600 mb-6">
An unexpected error occurred. Our team has been notified.
</p>
<div className="flex gap-4 justify-center">
<button
onClick={reset}
className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700"
>
Try Again
</button>
<a
href="/"
className="bg-gray-600 text-white px-6 py-2 rounded hover:bg-gray-700"
>
Go Home
</a>
</div>
</div>
</div>
</body>
</html>
);
}
Important: The root error boundary must include <html> and <body> tags since it replaces the entire page.
Step 2: Section-Level Error Boundary
For a blog section, catch errors specific to that area:
// app/blog/error.tsx
"use client";
export default function BlogError({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="max-w-2xl mx-auto p-8">
<div className="bg-red-50 border border-red-200 rounded-lg p-6">
<h2 className="text-xl font-bold text-red-600 mb-2">
Blog Error
</h2>
<p className="text-gray-700 mb-4">
Failed to load the blog. Please try again.
</p>
<details className="mb-4 text-sm">
<summary className="cursor-pointer text-red-600 font-semibold">
Error Details
</summary>
<pre className="mt-2 p-2 bg-gray-100 rounded overflow-auto text-xs">
{error.message}
</pre>
</details>
<button
onClick={reset}
className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700"
>
Reload Blog
</button>
</div>
</div>
);
}
Step 3: Trigger and Handle Errors
Create a page that may throw an error:
// app/blog/page.tsx
async function getBlogPosts() {
const res = await fetch("https://api.example.com/posts", {
cache: "no-store",
});
if (!res.ok) {
throw new Error(`Failed to fetch posts: ${res.status}`);
}
return res.json();
}
export default async function BlogPage() {
const posts = await getBlogPosts();
return (
<div className="max-w-4xl mx-auto p-8">
<h1 className="text-4xl font-bold mb-8">Blog</h1>
<div className="space-y-6">
{posts.map((post: any) => (
<article key={post.id} className="p-6 border rounded-lg">
<h2 className="text-2xl font-bold">{post.title}</h2>
<p className="text-gray-600">{post.excerpt}</p>
</article>
))}
</div>
</div>
);
}
If the API request fails, the error is thrown. The nearest error.tsx (app/blog/error.tsx) catches it and displays the error UI. Users see the recovery button instead of a crash.
Error Logging and Monitoring
In production, log errors to a monitoring service like Sentry, Datadog, or LogRocket:
// app/error.tsx
"use client";
import { useEffect } from "react";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
useEffect(() => {
// Log to Sentry
if (typeof window !== "undefined" && window.Sentry) {
window.Sentry.captureException(error);
}
// Or log to a custom API endpoint
fetch("/api/log-error", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
}),
}).catch(console.error);
}, [error]);
return (
<div className="flex items-center justify-center min-h-screen bg-gray-100">
<div className="bg-white p-8 rounded-lg shadow-lg text-center">
<h2 className="text-2xl font-bold text-red-600 mb-2">Error Occurred</h2>
<p className="text-gray-600 mb-6">
We've logged this issue and will investigate.
</p>
<button
onClick={reset}
className="bg-blue-600 text-white px-6 py-2 rounded"
>
Try Again
</button>
</div>
</div>
);
}
Handling 404 Errors
For missing routes, create a not-found.tsx file (separate from error.tsx). It's triggered when no matching route exists:
// app/not-found.tsx
export default function NotFound() {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h1 className="text-6xl font-bold text-gray-800 mb-2">404</h1>
<p className="text-xl text-gray-600 mb-8">Page not found</p>
<a href="/" className="bg-blue-600 text-white px-6 py-2 rounded">
Go Home
</a>
</div>
);
}
not-found.tsx is distinct from error.tsx. Use not-found.tsx for missing pages; use error.tsx for runtime errors in pages that do exist.
Best Practices
1. Be Specific in Error Messages – Help users understand what went wrong:
// Good
<p>Failed to load the blog post. Check your internet and try again.</p>
// Bad
<p>Error</p>
2. Offer Recovery – Always provide a "Try Again" button or a way to navigate elsewhere:
<button onClick={reset}>Retry</button>
<a href="/">Go Home</a>
3. Log Errors for Debugging – In development, show error details; in production, log to a service:
{process.env.NODE_ENV === "development" && (
<details>
<summary>Error Details</summary>
<pre>{error.stack}</pre>
</details>
)}
4. Nest Error Boundaries Strategically – Not every folder needs an error boundary. Add them at section boundaries (blog, dashboard, etc.) and at the root.
Key Takeaways
error.tsxwraps child routes in an error boundary, catching exceptions and displaying recovery UI.errorprop contains the thrown error;resetfunction re-renders the page and clears the error state.- Nested boundaries let each section handle errors independently; the nearest error.tsx catches the error.
not-found.tsxhandles 404 routes (missing pages), distinct from runtime errors.- Error logging to external services helps identify and fix issues in production.
Frequently Asked Questions
Can error.tsx catch Server Component errors?
Yes. Any error thrown in Server Components (including during data fetching) bubbles up to the nearest error.tsx boundary.
Do error boundaries catch client-side JavaScript errors?
Yes. Both server-side and client-side errors are caught by error.tsx, as long as the component is marked with "use client".
What if error.tsx itself throws an error?
The error bubbles up to the parent error.tsx. If the root error.tsx fails, Next.js shows its default error page.
Can I access route params in error.tsx?
No. error.tsx doesn't receive params. It only gets the error object and reset function. If you need context, pass it via a context provider in the layout.
Does error.tsx work for API routes?
No. API routes use try-catch blocks to handle errors manually and return error responses (e.g., NextResponse.json({ error: "..." }, { status: 500 })).
Further Reading
- Next.js Error Handling Documentation – official comprehensive guide.
- Sentry Integration for Next.js – error monitoring in production.
- React Error Boundaries – foundational error handling concepts.