Things go wrong. APIs fail, data is malformed, networks drop. Without proper error handling, your entire app could crash with a cryptic white screen. Next.js makes error handling elegant with error.tsx files that automatically create React Error Boundaries. When an error occurs, users see a helpful error UI with recovery options—not a broken page. Let's build resilient applications that handle failures gracefully and keep users informed!
What Is error.tsx?
error.tsx is a special file that defines error UI for a route segment:
- Automatic Error Boundary: Next.js wraps pages with React Error Boundary
- Catches runtime errors: Any error in page or children
- Recovery option: Reset function to retry the operation
- Keeps layout intact: Error replaces page content, not layout
Error Handling Structure
Each route segment can have its own error.tsx
Select a file or folder to see details
How It Works
When you create error.tsx, Next.js automatically wraps your page:
<Layout>
<ErrorBoundary fallback={<YourErrorUI />}>
<Page />
</ErrorBoundary>
</Layout>If an error occurs in the page, it's caught and your error UI shows instead!
Creating Your First Error Handler
Step 1: Basic Error Component
'use client'; // Error components must be Client Components
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50">
<div className="text-center">
<h2 className="text-2xl font-bold text-red-600 mb-4">
Something went wrong!
</h2>
<p className="text-gray-600 mb-6">
{error.message}
</p>
<button
onClick={reset}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Try again
</button>
</div>
</div>
);
}⚠️ Must Be Client Component
Error components must have 'use client' at the top because they use hooks and event handlers.
Step 2: Better Error UI
'use client';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Log error to error reporting service
console.error('Error:', error);
}, [error]);
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50 px-4">
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8">
{/* Error icon */}
<div className="flex justify-center mb-6">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center">
<svg
className="w-8 h-8 text-red-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
</div>
{/* Error heading */}
<h2 className="text-2xl font-bold text-center text-gray-900 mb-2">
Oops! Something went wrong
</h2>
{/* Error message */}
<p className="text-center text-gray-600 mb-6">
{error.message || 'An unexpected error occurred'}
</p>
{/* Error ID (if available) */}
{error.digest && (
<p className="text-center text-sm text-gray-500 mb-6">
Error ID: {error.digest}
</p>
)}
{/* Action buttons */}
<div className="flex flex-col sm:flex-row gap-3">
<button
onClick={reset}
className="flex-1 px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
>
Try Again
</button>
href="/"
className="flex-1 px-6 py-3 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition text-center"
>
Go Home
</a>
</div>
{/* Help text */}
<p className="text-center text-sm text-gray-500 mt-6">
If this problem persists, please{' '}
<a href="/contact" className="text-blue-600 hover:underline">
contact support
</a>
</p>
</div>
</div>
);
}Understanding the Props
error: Error & { digest?: string }
error.message: Human-readable error message
error.digest: Unique error ID (production only)
error.stack: Stack trace (development only)
reset: () => void
Function to attempt recovery. When called, Next.js will try to re-render the error boundary's contents.
Handling Different Error Scenarios
1. Network Errors
'use client';
export default function BlogError({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
const isNetworkError = error.message.includes('fetch') ||
error.message.includes('network');
return (
<div className="container mx-auto px-4 py-12">
<div className="max-w-lg mx-auto text-center">
<h2 className="text-2xl font-bold mb-4">
{isNetworkError
? 'Connection Problem'
: 'Something Went Wrong'}
</h2>
<p className="text-gray-600 mb-6">
{isNetworkError
? 'We couldn\'t load the blog posts. Please check your internet connection.'
: error.message}
</p>
<button
onClick={reset}
className="px-6 py-3 bg-blue-600 text-white rounded"
>
{isNetworkError ? 'Retry Connection' : 'Try Again'}
</button>
</div>
</div>
);
}2. Not Found vs Error
'use client';
export default function PostError({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
const isNotFound = error.message.includes('NEXT_NOT_FOUND');
if (isNotFound) {
return (
<div className="container mx-auto px-4 py-12">
<h2 className="text-2xl font-bold mb-4">Post Not Found</h2>
<p className="mb-6">This blog post doesn't exist.</p>
<a href="/blog" className="text-blue-600 hover:underline">
← Back to Blog
</a>
</div>
);
}
return (
<div className="container mx-auto px-4 py-12">
<h2 className="text-2xl font-bold mb-4">Error Loading Post</h2>
<p className="mb-6">{error.message}</p>
<button onClick={reset} className="px-6 py-3 bg-blue-600 text-white rounded">
Try Again
</button>
</div>
);
}3. Authentication Errors
'use client';
import { useRouter } from 'next/navigation';
export default function DashboardError({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
const router = useRouter();
const isAuthError = error.message.includes('Unauthorized') ||
error.message.includes('Authentication');
if (isAuthError) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h2 className="text-2xl font-bold mb-4">
Authentication Required
</h2>
<p className="text-gray-600 mb-6">
Please log in to access the dashboard.
</p>
<button
onClick={() => router.push('/login')}
className="px-6 py-3 bg-blue-600 text-white rounded"
>
Go to Login
</button>
</div>
</div>
);
}
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h2 className="text-2xl font-bold mb-4">Dashboard Error</h2>
<p className="text-gray-600 mb-6">{error.message}</p>
<button onClick={reset} className="px-6 py-3 bg-blue-600 text-white rounded">
Try Again
</button>
</div>
</div>
);
}Nested Error Boundaries
Like loading states, errors cascade from specific to general:
app/
error.tsx ← Catches all app errors
dashboard/
error.tsx ← Catches dashboard errors
analytics/
error.tsx ← Catches only analytics errors
page.tsxError in: /dashboard/analytics
<RootLayout>
<DashboardLayout> ← Layout stays intact
<ErrorBoundary fallback={<AnalyticsError />}>
<AnalyticsPage /> ← Error caught here
</ErrorBoundary>
</DashboardLayout>
</RootLayout>
Shows: Analytics error UI
Dashboard layout remains visible!Layout Errors
error.tsx does NOT catch errors in layouts at the same level. To catch layout errors, you need an error.tsx in the parent folder.
Global Error Handler
For errors in the root layout, use global-error.tsx:
'use client';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<div className="flex items-center justify-center min-h-screen bg-gray-50 px-4">
<div className="max-w-md w-full text-center">
<h2 className="text-3xl font-bold text-red-600 mb-4">
Application Error
</h2>
<p className="text-gray-600 mb-6">
A critical error occurred. Please try refreshing the page.
</p>
<button
onClick={reset}
className="px-6 py-3 bg-blue-600 text-white rounded-lg"
>
Refresh Application
</button>
</div>
</div>
</body>
</html>
);
}Important: global-error.tsx Must Include HTML
Since it replaces the root layout when active, global-error.tsx must include <html> and <body> tags.
Error Logging and Monitoring
Logging to External Service
'use client';
import { useEffect } from 'react';
import * as Sentry from '@sentry/nextjs';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
useEffect(() => {
// Log to Sentry
Sentry.captureException(error);
// Or log to your custom service
fetch('/api/log-error', {
method: 'POST',
body: JSON.stringify({
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
}),
});
}, [error]);
return (
<div>
<h2>Error occurred</h2>
<button onClick={reset}>Try again</button>
</div>
);
}API Route for Error Logging
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const error = await request.json();
// Log to your database or logging service
console.error('Client Error:', {
message: error.message,
stack: error.stack,
timestamp: error.timestamp,
userAgent: error.userAgent,
});
// Could also send to external service
// await sendToLoggingService(error);
return NextResponse.json({ success: true });
} catch (err) {
return NextResponse.json(
{ error: 'Failed to log error' },
{ status: 500 }
);
}
}Error Recovery Patterns
1. Simple Reset
<button onClick={reset}>
Try Again
</button>2. Reset with Loading State
'use client';
import { useState } from 'react';
export default function Error({ error, reset }) {
const [isResetting, setIsResetting] = useState(false);
const handleReset = async () => {
setIsResetting(true);
await new Promise(resolve => setTimeout(resolve, 500));
reset();
};
return (
<div>
<h2>Error: {error.message}</h2>
<button
onClick={handleReset}
disabled={isResetting}
className="px-6 py-3 bg-blue-600 text-white rounded disabled:opacity-50"
>
{isResetting ? 'Retrying...' : 'Try Again'}
</button>
</div>
);
}3. Alternative Actions
'use client';
import { useRouter } from 'next/navigation';
export default function Error({ error, reset }) {
const router = useRouter();
return (
<div>
<h2>Error Loading Data</h2>
<p>{error.message}</p>
<div className="flex gap-4">
<button onClick={reset}>
Try Again
</button>
<button onClick={() => router.back()}>
Go Back
</button>
<button onClick={() => router.push('/')}>
Go Home
</button>
</div>
</div>
);
}4. Retry with Exponential Backoff
'use client';
import { useState } from 'react';
export default function Error({ error, reset }) {
const [retryCount, setRetryCount] = useState(0);
const [isRetrying, setIsRetrying] = useState(false);
const handleRetry = async () => {
setIsRetrying(true);
setRetryCount(prev => prev + 1);
// Exponential backoff: 1s, 2s, 4s, 8s
const delay = Math.min(1000 * Math.pow(2, retryCount), 8000);
await new Promise(resolve => setTimeout(resolve, delay));
reset();
setIsRetrying(false);
};
return (
<div>
<h2>Error: {error.message}</h2>
<p>Retry attempt: {retryCount}</p>
<button
onClick={handleRetry}
disabled={isRetrying}
>
{isRetrying ? 'Retrying...' : 'Try Again'}
</button>
</div>
);
}Error Handling Best Practices
1. Provide Clear Information
// ✅ Good: Clear, actionable
<div>
<h2>Failed to Load Posts</h2>
<p>We couldn't connect to the server. Please check your connection and try again.</p>
<button onClick={reset}>Retry</button>
</div>
// ❌ Bad: Vague, unhelpful
<div>
<h2>Error</h2>
<p>Something went wrong</p>
</div>2. Show Different Messages for Production
'use client';
export default function Error({ error, reset }) {
const isDev = process.env.NODE_ENV === 'development';
return (
<div>
<h2>Something went wrong</h2>
{/* Show detailed errors in development */}
{isDev && (
<details className="mt-4 p-4 bg-gray-100 rounded">
<summary>Error details</summary>
<pre className="mt-2 text-xs overflow-auto">
{error.message}
{error.stack}
</pre>
</details>
)}
{/* Generic message in production */}
{!isDev && (
<p>An unexpected error occurred. Our team has been notified.</p>
)}
<button onClick={reset}>Try Again</button>
</div>
);
}3. Maintain Brand Consistency
// Match your app's design system
export default function Error({ error, reset }) {
return (
<div className="container mx-auto px-4 py-12">
<div className="max-w-lg mx-auto">
{/* Use your brand colors */}
<div className="bg-red-50 border-l-4 border-red-500 p-6 rounded">
<h2 className="text-2xl font-bold text-red-900 mb-2">
Oops!
</h2>
<p className="text-red-700">
{error.message}
</p>
</div>
{/* Use your button styles */}
<button
onClick={reset}
className="mt-6 btn btn-primary" // Your classes
>
Try Again
</button>
</div>
</div>
);
}4. Test Error States
// Add a test route that throws errors
// app/test-error/page.tsx
export default function TestError() {
throw new Error('Test error for development');
return null;
}
// Visit /test-error to see your error UI5. Provide Context-Appropriate Actions
- Network errors: Retry button
- Auth errors: Login button
- Not found errors: Back or home link
- Permission errors: Contact support link
Complete Practical Example
Production-Ready Error Component
A complete error UI with logging and recovery options
Output Preview
Special Case: not-found.tsx
For 404 errors, use not-found.tsx instead of error.tsx:
import Link from 'next/link';
export default function NotFound() {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50">
<div className="text-center">
<h1 className="text-6xl font-bold text-gray-900 mb-4">404</h1>
<h2 className="text-2xl font-semibold text-gray-700 mb-4">
Page Not Found
</h2>
<p className="text-gray-600 mb-8">
The page you're looking for doesn't exist.
</p>
<Link
href="/"
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Go Home
</Link>
</div>
</div>
);
}Trigger not-found programmatically:
import { notFound } from 'next/navigation';
export default async function Page({ params }) {
const post = await getPost(params.slug);
if (!post) {
notFound(); // Shows not-found.tsx
}
return <div>{post.content}</div>;
}Key Takeaways
- error.tsx creates automatic Error Boundaries - no manual setup
- Must be Client Components - use 'use client' directive
- Receives error and reset props - show error, provide recovery
- Each route segment can have its own error handler - granular errors
- Layouts stay intact - only page content shows error
- Use global-error.tsx for root layout errors - must include HTML
- Log errors for monitoring - track issues in production
- Provide clear, actionable error messages - help users recover
You've Completed Layouts & Pages! 🎉
Congratulations! You've finished the entire Layouts & Pages section, mastering:
What You've Learned
📐 Layouts
- Understanding layouts
- Root layout configuration
- Nested layouts
- Templates vs layouts
⚡ Loading & Errors
- Loading states with loading.tsx
- Error handling with error.tsx
- Skeleton screens
- Error boundaries
You now have all the tools to build sophisticated Next.js applications with proper structure, loading states, and error handling. Your apps will be resilient, user-friendly, and production-ready!
🚀 What's Next?
You've mastered the fundamentals of layouts and pages. The next topics in your Next.js journey will cover data fetching, server and client components, caching, and more advanced patterns. You're building a solid foundation!