Errors happen—resources don't exist, data fails to load, or unexpected issues occur. Custom error pages provide better user experiences than generic error screens. Create not-found.tsx for 404 pages, error.tsx for error boundaries, and global-error.tsx for root-level errors. Build helpful, branded error pages that guide users back to working parts of your app!
Not Found Pages (404)
Root not-found.tsx
import Link from 'next/link';
export default function NotFound() {
return (
<div className="flex min-h-screen flex-col items-center justify-center">
<h1 className="text-6xl font-bold text-gray-900 mb-4">404</h1>
<h2 className="text-2xl font-semibold text-gray-700 mb-2">
Page Not Found
</h2>
<p className="text-gray-600 mb-8 text-center max-w-md">
Sorry, we couldn't find the page you're looking for.
</p>
<div className="flex gap-4">
<Link
href="/"
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Go Home
</Link>
<Link
href="/contact"
className="px-6 py-3 border border-gray-300 rounded-lg hover:bg-gray-50"
>
Contact Support
</Link>
</div>
</div>
);
}
// ✅ Shown when route doesn't exist
// ✅ Custom 404 UI
// ✅ Navigation links
// ✅ Branded experienceTriggering notFound() Programmatically
import { notFound } from 'next/navigation';
async function getPost(slug: string) {
const post = await db.posts.findUnique({
where: { slug },
});
return post;
}
export default async function BlogPostPage({
params,
}: {
params: { slug: string };
}) {
const post = await getPost(params.slug);
// Trigger 404 if post doesn't exist
if (!post) {
notFound();
}
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
// ✅ Check if resource exists
// ✅ Call notFound() if not found
// ✅ Shows not-found.tsx
// ✅ Returns 404 status codeRoute-Specific not-found.tsx
import Link from 'next/link';
export default function DashboardNotFound() {
return (
<div className="p-8">
<h1 className="text-4xl font-bold mb-4">Dashboard Page Not Found</h1>
<p className="text-gray-600 mb-6">
The dashboard page you're looking for doesn't exist.
</p>
<div className="flex gap-4">
<Link
href="/dashboard"
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Dashboard Home
</Link>
<Link
href="/dashboard/settings"
className="px-4 py-2 border rounded hover:bg-gray-50"
>
Settings
</Link>
</div>
</div>
);
}
// ✅ Specific to dashboard routes
// ✅ Contextual navigation
// ✅ Maintains dashboard layout
// ✅ More helpful than generic 404Nested not-found.tsx
// File structure:
app/
not-found.tsx // Root 404
dashboard/
not-found.tsx // Dashboard 404
[id]/
not-found.tsx // Specific dashboard item 404
// Priority (most specific first):
// 1. /dashboard/[id]/not-found.tsx - Most specific
// 2. /dashboard/not-found.tsx - Section-specific
// 3. /app/not-found.tsx - Root fallback
// ✅ More specific not-found.tsx overrides parent
// ✅ Contextual error messages
// ✅ Appropriate navigation optionsError Boundaries (error.tsx)
Basic error.tsx
'use client'; // Error components must be Client Components
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 min-h-screen flex-col items-center justify-center p-8">
<h2 className="text-3xl font-bold text-gray-900 mb-4">
Something went wrong!
</h2>
<p className="text-gray-600 mb-8 text-center max-w-md">
We're sorry, but something unexpected happened. Please try again.
</p>
<button
onClick={reset}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Try Again
</button>
</div>
);
}
// ✅ Must be Client Component ('use client')
// ✅ Receives error and reset props
// ✅ reset() attempts to re-render
// ✅ Catches errors in route segmentError with More Details
'use client';
import { useEffect } from 'react';
import Link from 'next/link';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Log to error reporting service
console.error('Error caught by error boundary:', {
message: error.message,
digest: error.digest,
stack: error.stack,
});
}, [error]);
return (
<div className="flex min-h-screen flex-col items-center justify-center p-8">
<div className="max-w-lg w-full">
<h1 className="text-4xl font-bold text-red-600 mb-4">
Oops! Something Went Wrong
</h1>
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<p className="font-semibold text-red-800 mb-2">Error Details:</p>
<p className="text-red-700 text-sm">{error.message}</p>
{error.digest && (
<p className="text-red-600 text-xs mt-2">
Error ID: {error.digest}
</p>
)}
</div>
<div className="flex gap-4">
<button
onClick={reset}
className="flex-1 px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Try Again
</button>
<Link
href="/"
className="flex-1 px-4 py-3 border border-gray-300 text-center rounded-lg hover:bg-gray-50"
>
Go Home
</Link>
</div>
<Link
href="/contact"
className="block text-center text-blue-600 hover:underline mt-6"
>
Contact Support
</Link>
</div>
</div>
);
}
// ✅ Shows error message
// ✅ Error ID for support
// ✅ Multiple recovery options
// ✅ Support linkRoute-Specific error.tsx
'use client';
import { useEffect } from 'react';
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error('Dashboard error:', error);
}, [error]);
return (
<div className="p-8">
<div className="max-w-2xl mx-auto">
<h1 className="text-3xl font-bold mb-4">Dashboard Error</h1>
<p className="text-gray-600 mb-6">
We encountered an error loading your dashboard. This might be a
temporary issue.
</p>
<div className="bg-yellow-50 border border-yellow-200 rounded p-4 mb-6">
<p className="text-yellow-800 text-sm">
{error.message}
</p>
</div>
<div className="flex gap-4">
<button
onClick={reset}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Reload Dashboard
</button>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 border rounded hover:bg-gray-50"
>
Refresh Page
</button>
</div>
</div>
</div>
);
}
// ✅ Dashboard-specific error UI
// ✅ Contextual messaging
// ✅ Maintains dashboard layout
// ✅ Multiple recovery optionsError Hierarchy
// File structure:
app/
error.tsx // Root error handler
layout.tsx // Root layout (NOT caught by error.tsx)
dashboard/
error.tsx // Dashboard errors
layout.tsx // Dashboard layout (caught by parent error.tsx)
[id]/
error.tsx // Specific dashboard item errors
page.tsx // Page (caught by closest error.tsx)
// Error boundary hierarchy:
// 1. Errors in page.tsx caught by closest error.tsx
// 2. Errors in layout.tsx caught by parent error.tsx
// 3. Errors in root layout.tsx caught by global-error.tsx
// ✅ Closest error.tsx catches error
// ✅ Layouts caught by parent error.tsx
// ✅ Root layout needs global-error.tsxGlobal Error Handler
global-error.tsx
'use client';
import { useEffect } from 'react';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error('Global error:', error);
}, [error]);
return (
<html>
<body>
<div className="flex min-h-screen flex-col items-center justify-center p-8">
<h1 className="text-4xl font-bold text-red-600 mb-4">
Critical Error
</h1>
<p className="text-gray-600 mb-8 max-w-md text-center">
A critical error occurred. Please try reloading the page.
</p>
<button
onClick={reset}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Reload Page
</button>
</div>
</body>
</html>
);
}
// ✅ Catches errors in root layout
// ✅ Must include <html> and <body>
// ✅ Only used in production
// ✅ Fallback for catastrophic errors⚠️ global-error.tsx Requirements
- Must be a Client Component (use 'use client')
- Must include full HTML structure (html, body tags)
- Only catches errors in root layout.tsx
- Only active in production (development shows overlay)
- Replaces entire page including root layout
When to Use global-error.tsx
// global-error.tsx catches errors that error.tsx cannot:
// ❌ error.tsx CANNOT catch:
app/
layout.tsx // Root layout errors
error.tsx // Cannot catch its parent layout
// ✅ global-error.tsx CATCHES:
app/
layout.tsx // Root layout errors
global-error.tsx // Catches root layout errors
// Use cases:
// - Database connection errors in root layout
// - Authentication provider errors
// - Critical configuration errors
// - Any error in root layout.tsx
// Most apps need:
// 1. error.tsx for route errors
// 2. global-error.tsx for root layout errorsError Pages File Structure
Organization of not-found and error pages
Select a file or folder to see details
Practical Error Page Examples
Example 1: Product Not Found
import { notFound } from 'next/navigation';
async function getProduct(id: string) {
const product = await db.products.findUnique({
where: { id },
});
return product;
}
export default async function ProductPage({
params,
}: {
params: { id: string };
}) {
const product = await getProduct(params.id);
if (!product) {
notFound();
}
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p className="text-2xl font-bold">${product.price}</p>
</div>
);
}
// ✅ Check product exists
// ✅ Call notFound() if missing
// ✅ Shows not-found.tsximport Link from 'next/link';
export default function ProductNotFound() {
return (
<div className="max-w-2xl mx-auto p-8 text-center">
<div className="mb-8">
<svg
className="w-24 h-24 mx-auto text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
/>
</svg>
</div>
<h1 className="text-3xl font-bold mb-4">Product Not Found</h1>
<p className="text-gray-600 mb-8">
Sorry, the product you're looking for doesn't exist or has been removed.
</p>
<div className="flex gap-4 justify-center">
<Link
href="/products"
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Browse Products
</Link>
<Link
href="/search"
className="px-6 py-3 border border-gray-300 rounded-lg hover:bg-gray-50"
>
Search
</Link>
</div>
</div>
);
}
// ✅ Product-specific 404
// ✅ Icon for visual feedback
// ✅ Helpful navigation
// ✅ Contextual messagingExample 2: API Error with Retry
'use client';
import { useEffect, useState } from 'react';
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const [retrying, setRetrying] = useState(false);
useEffect(() => {
console.error('Dashboard error:', error);
}, [error]);
const handleRetry = async () => {
setRetrying(true);
// Wait a bit before retrying
await new Promise(resolve => setTimeout(resolve, 1000));
reset();
setRetrying(false);
};
return (
<div className="max-w-2xl mx-auto p-8">
<div className="bg-red-50 border-2 border-red-200 rounded-lg p-6 mb-6">
<h2 className="text-2xl font-bold text-red-800 mb-2">
Failed to Load Dashboard
</h2>
<p className="text-red-700 mb-4">
{error.message || 'An unexpected error occurred'}
</p>
{error.digest && (
<p className="text-red-600 text-sm">
Error Reference: {error.digest}
</p>
)}
</div>
<div className="space-y-4">
<button
onClick={handleRetry}
disabled={retrying}
className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{retrying ? 'Retrying...' : 'Try Again'}
</button>
<button
onClick={() => window.location.href = '/'}
className="w-full px-6 py-3 border border-gray-300 rounded-lg hover:bg-gray-50"
>
Return to Home
</button>
</div>
<div className="mt-8 text-center text-sm text-gray-600">
<p>Still having issues?</p>
href="/contact"
className="text-blue-600 hover:underline"
>
Contact our support team
</a>
</div>
</div>
);
}
// ✅ Retry with loading state
// ✅ Error details visible
// ✅ Multiple escape routes
// ✅ Support linkExample 3: User-Friendly Error Messages
'use client';
import { useEffect } from 'react';
function getErrorMessage(error: Error): {
title: string;
message: string;
action: string;
} {
// Parse error for user-friendly messages
if (error.message.includes('fetch failed')) {
return {
title: 'Connection Error',
message: 'We couldn't connect to our servers. Please check your internet connection.',
action: 'Try again when you're back online.',
};
}
if (error.message.includes('timeout')) {
return {
title: 'Request Timeout',
message: 'The request took too long to complete.',
action: 'Please try again.',
};
}
if (error.message.includes('unauthorized')) {
return {
title: 'Authentication Required',
message: 'Your session may have expired.',
action: 'Please log in again.',
};
}
// Default message
return {
title: 'Something Went Wrong',
message: 'We encountered an unexpected error.',
action: 'Please try again or contact support if the problem persists.',
};
}
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error('Error:', error);
}, [error]);
const { title, message, action } = getErrorMessage(error);
return (
<div className="flex min-h-screen items-center justify-center p-4">
<div className="max-w-md w-full">
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 bg-red-100 rounded-full mb-4">
<svg
className="w-8 h-8 text-red-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<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>
<h1 className="text-2xl font-bold text-gray-900 mb-2">
{title}
</h1>
<p className="text-gray-600 mb-1">
{message}
</p>
<p className="text-sm text-gray-500">
{action}
</p>
</div>
<button
onClick={reset}
className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Try Again
</button>
</div>
</div>
);
}
// ✅ User-friendly error messages
// ✅ Specific guidance based on error
// ✅ Clean, professional UI
// ✅ Icon for visual feedbackError Recovery Patterns
Automatic Retry Pattern
'use client';
import { useEffect, useState } from 'react';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const [retryCount, setRetryCount] = useState(0);
const maxRetries = 3;
useEffect(() => {
console.error('Error:', error);
// Auto-retry for transient errors
if (retryCount < maxRetries && isTransientError(error)) {
const timeout = setTimeout(() => {
console.log(`Auto-retry attempt ${retryCount + 1}`);
setRetryCount(prev => prev + 1);
reset();
}, 2000 * (retryCount + 1)); // Exponential backoff
return () => clearTimeout(timeout);
}
}, [error, reset, retryCount]);
return (
<div className="p-8 text-center">
{retryCount < maxRetries ? (
<div>
<h2 className="text-xl font-semibold mb-2">Attempting to recover...</h2>
<p className="text-gray-600">
Retry attempt {retryCount + 1} of {maxRetries}
</p>
</div>
) : (
<div>
<h2 className="text-xl font-semibold mb-2">Unable to Recover</h2>
<p className="text-gray-600 mb-4">{error.message}</p>
<button
onClick={() => {
setRetryCount(0);
reset();
}}
className="px-6 py-3 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Try Again
</button>
</div>
)}
</div>
);
}
function isTransientError(error: Error): boolean {
const transientErrors = ['fetch failed', 'timeout', 'ECONNREFUSED'];
return transientErrors.some(msg => error.message.includes(msg));
}
// ✅ Automatic retry for transient errors
// ✅ Exponential backoff
// ✅ Max retry limit
// ✅ Manual retry optionPartial Error Recovery
'use client';
import { useState } from 'react';
import Link from 'next/link';
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const [showDetails, setShowDetails] = useState(false);
return (
<div className="max-w-4xl mx-auto p-8">
<div className="mb-8">
<h1 className="text-3xl font-bold mb-2">Dashboard Unavailable</h1>
<p className="text-gray-600">
We're having trouble loading your dashboard. You can still access other parts of the app.
</p>
</div>
{/* Alternative actions */}
<div className="grid grid-cols-2 gap-4 mb-8">
<Link
href="/profile"
className="p-4 border rounded-lg hover:bg-gray-50 text-center"
>
<h3 className="font-semibold mb-1">View Profile</h3>
<p className="text-sm text-gray-600">Check your profile settings</p>
</Link>
<Link
href="/history"
className="p-4 border rounded-lg hover:bg-gray-50 text-center"
>
<h3 className="font-semibold mb-1">View History</h3>
<p className="text-sm text-gray-600">See your recent activity</p>
</Link>
</div>
<button
onClick={reset}
className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 mb-4"
>
Try Loading Dashboard Again
</button>
<button
onClick={() => setShowDetails(!showDetails)}
className="text-sm text-gray-600 hover:underline"
>
{showDetails ? 'Hide' : 'Show'} Error Details
</button>
{showDetails && (
<div className="mt-4 p-4 bg-gray-100 rounded text-sm">
<p className="font-mono">{error.message}</p>
{error.digest && (
<p className="mt-2 text-gray-600">ID: {error.digest}</p>
)}
</div>
)}
</div>
);
}
// ✅ Alternative actions available
// ✅ Doesn't block entire app
// ✅ Optional error details
// ✅ Multiple recovery pathsError Page Best Practices
1. Provide Clear Next Steps
// ✅ GOOD: Clear actions
<div>
<h2>Page Not Found</h2>
<p>The page you're looking for doesn't exist.</p>
<Link href="/">Go to Homepage</Link>
<Link href="/search">Search</Link>
<Link href="/contact">Contact Support</Link>
</div>
// ❌ BAD: No guidance
<div>
<h2>404</h2>
<p>Not found</p>
</div>
// Always provide actionable next steps2. Match Your Brand
// ✅ GOOD: Branded error page
export default function NotFound() {
return (
<div className="min-h-screen bg-gradient-to-b from-blue-50 to-white">
<nav>{/* Your app's navbar */}</nav>
<div className="container mx-auto">
{/* Branded 404 content */}
</div>
<footer>{/* Your app's footer */}</footer>
</div>
);
}
// ❌ BAD: Generic error page
<div>404 Not Found</div>
// Make error pages feel part of your app3. Log Errors Properly
// ✅ GOOD: Comprehensive logging
useEffect(() => {
console.error('Error details:', {
message: error.message,
digest: error.digest,
stack: error.stack,
timestamp: new Date().toISOString(),
url: window.location.href,
userAgent: navigator.userAgent,
});
// Send to error tracking service
if (process.env.NODE_ENV === 'production') {
logToErrorService(error);
}
}, [error]);
// Error tracking helps fix issues4. Avoid Exposing Sensitive Info
// ✅ GOOD: Generic user-facing message
<p>We encountered an error loading your data.</p>
{error.digest && <p>Reference: {error.digest}</p>}
// ❌ BAD: Exposing internals
<p>Database connection failed: {databaseUrl}</p>
<p>API key invalid: {apiKey}</p>
// Never expose:
// - Database URLs
// - API keys
// - Internal paths
// - Stack traces (in production)5. Test Error Pages
// Test not-found page:
// Visit /this-page-does-not-exist
// Test error.tsx:
// Create component that throws error
function BrokenComponent() {
throw new Error('Test error');
return <div>Never rendered</div>;
}
// Test in development:
export default function TestPage() {
return (
<div>
<h1>Test Error Handling</h1>
<BrokenComponent />
</div>
);
}
// ✅ Test all error scenarios
// ✅ Verify error pages work
// ✅ Check error loggingKey Takeaways
- not-found.tsx - custom 404 pages, triggered by notFound()
- error.tsx - error boundaries for route segments (Client Component)
- global-error.tsx - catches root layout errors (includes html/body)
- Nested error pages - more specific overrides less specific
- reset() function - attempt to recover from errors
- User-friendly messages - avoid technical jargon
- Clear next steps - provide navigation and support links
- Log errors - track issues for debugging
🎉 Middleware and Advanced Features Complete!
You've completed the Middleware and Advanced Features section! You've mastered:
- ✅ Introduction to Middleware
- ✅ Authentication with Middleware
- ✅ Environment Variables and Configuration
- ✅ Streaming and Suspense
- ✅ Not Found and Global Error Pages
You now have complete mastery of advanced Next.js features! You can intercept and modify requests with middleware, implement authentication flows, manage environment variables securely, use streaming and Suspense for progressive rendering, and create custom error pages for robust error handling. These advanced skills enable you to build production-ready applications with excellent user experiences and proper error handling!
🎓 Congratulations!
You've completed 62 comprehensive tutorials covering the complete Next.js 15 framework! From routing fundamentals to advanced middleware patterns, from styling to API development, from data fetching to error handling—you've learned it all. You're now equipped to build professional, production-ready Next.js applications!