Next.js provides multiple error handling mechanisms to help developers build robust applications. Understanding these mechanisms is crucial for building reliable user experiences.
Error Handling Layers
Next.js error handling is divided into multiple layers:
- Component-level errors: Using Error Boundary
- Route-level errors: Using error.js file
- Server-level errors: Using global error handling
- API errors: Handling in API Routes
Route-level Error Handling (App Router)
error.js File
In App Router, the error.js file is used to catch errors in route segments and their child components.
javascript// app/error.js 'use client'; export default function Error({ error, reset, }) { return ( <div> <h2>Something went wrong!</h2> <p>{error.message}</p> <button onClick={() => reset()}>Try again</button> </div> ); } export default function GlobalError({ error, reset }) { return ( <html> <body> <h2>Something went wrong!</h2> <button onClick={reset}>Try again</button> </body> </html> ); }
Nested Error Boundaries
javascript// app/dashboard/error.js 'use client'; export default function DashboardError({ error, reset }) { return ( <div> <h2>Dashboard Error</h2> <p>{error.message}</p> <button onClick={reset}>Retry</button> </div> ); } // app/dashboard/settings/error.js 'use client'; export default function SettingsError({ error, reset }) { return ( <div> <h2>Settings Error</h2> <p>{error.message}</p> <button onClick={reset}>Retry</button> </div> ); }
Pages Router Error Handling
Custom Error Pages
javascript// pages/404.js export default function Custom404() { return ( <div> <h1>404 - Page Not Found</h1> <p>The page you're looking for doesn't exist.</p> <Link href="/">Go Home</Link> </div> ); } // pages/500.js export default function Custom500() { return ( <div> <h1>500 - Server Error</h1> <p>Something went wrong on our end.</p> <Link href="/">Go Home</Link> </div> ); } // pages/_error.js export default function Error({ statusCode, err }) { return ( <div> <p> {statusCode ? `An error ${statusCode} occurred on server` : 'An error occurred on client'} </p> <p>{err?.message}</p> </div> ); } Error.getInitialProps = ({ res, err }) => { const statusCode = res ? res.statusCode : err ? err.statusCode : 404; return { statusCode }; };
getServerSideProps Error Handling
javascriptexport async function getServerSideProps(context) { try { const data = await fetchData(); if (!data) { return { notFound: true, // Returns 404 page }; } return { props: { data }, }; } catch (error) { return { props: { error: error.message }, }; } }
Server Component Error Handling
Using try-catch
javascriptasync function ProductPage({ params }) { try { const product = await fetchProduct(params.id); if (!product) { notFound(); // Returns 404 page } return <ProductDetails product={product} />; } catch (error) { console.error('Failed to fetch product:', error); throw error; // Triggers nearest error boundary } }
Using notFound()
javascriptimport { notFound } from 'next/navigation'; async function BlogPost({ params }) { const post = await getPostBySlug(params.slug); if (!post) { notFound(); // Returns 404 page } return <PostContent post={post} />; }
Custom not-found Page
javascript// app/not-found.js export default function NotFound() { return ( <div> <h2>Page Not Found</h2> <p>The page you're looking for doesn't exist.</p> <Link href="/">Go Home</Link> </div> ); } // app/blog/not-found.js export default function BlogNotFound() { return ( <div> <h2>Blog Post Not Found</h2> <p>The blog post you're looking for doesn't exist.</p> <Link href="/blog">Back to Blog</Link> </div> ); }
API Routes Error Handling
Error Response Format
javascript// app/api/hello/route.js export async function GET(request) { try { const data = await fetchData(); return Response.json(data); } catch (error) { console.error('API Error:', error); return Response.json( { error: 'Internal Server Error', message: error.message, timestamp: new Date().toISOString(), }, { status: 500 } ); } }
Validation Errors
javascriptimport { z } from 'zod'; const schema = z.object({ email: z.string().email(), password: z.string().min(8), }); export async function POST(request) { try { const body = await request.json(); // Validate input const validated = schema.parse(body); // Process request const result = await createUser(validated); return Response.json(result, { status: 201 }); } catch (error) { if (error instanceof z.ZodError) { return Response.json( { error: 'Validation Error', details: error.errors, }, { status: 400 } ); } return Response.json( { error: 'Internal Server Error' }, { status: 500 } ); } }
Authentication Errors
javascriptexport async function GET(request) { const token = request.headers.get('authorization'); if (!token) { return Response.json( { error: 'Unauthorized' }, { status: 401 } ); } try { const user = await verifyToken(token); return Response.json({ user }); } catch (error) { return Response.json( { error: 'Invalid token' }, { status: 401 } ); } }
Client-side Error Handling
Error Boundary Component
javascript'use client'; import { Component, ReactNode } from 'react'; interface Props { children: ReactNode; fallback?: ReactNode; } interface State { hasError: boolean; error?: Error; } export class ErrorBoundary extends Component<Props, State> { constructor(props: Props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error: Error): State { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: any) { console.error('Error caught by boundary:', error, errorInfo); // Send error to logging service logErrorToService(error, errorInfo); } render() { if (this.state.hasError) { return this.props.fallback || ( <div> <h2>Something went wrong</h2> <p>{this.state.error?.message}</p> <button onClick={() => this.setState({ hasError: false })}> Try again </button> </div> ); } return this.props.children; } }
Using Error Boundary
javascript'use client'; import { ErrorBoundary } from '@/components/ErrorBoundary'; export default function Page() { return ( <ErrorBoundary fallback={ <div> <h2>Component Error</h2> <button onClick={() => window.location.reload()}> Reload Page </button> </div> } > <RiskyComponent /> </ErrorBoundary> ); }
Async Error Handling
javascript'use client'; import { useEffect } from 'react'; export default function AsyncComponent() { useEffect(() => { const fetchData = async () => { try { const data = await fetch('/api/data').then(r => r.json()); console.log(data); } catch (error) { console.error('Fetch error:', error); // Show error message showErrorToast('Failed to fetch data'); } }; fetchData(); }, []); return <div>Loading...</div>; }
Global Error Handling
Using window.onerror
javascript// app/layout.js 'use client'; import { useEffect } from 'react'; export default function RootLayout({ children }) { useEffect(() => { const handleError = (event: ErrorEvent) => { console.error('Global error:', event.error); // Send to error tracking service logErrorToService({ message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno, error: event.error, }); }; window.addEventListener('error', handleError); return () => { window.removeEventListener('error', handleError); }; }, []); return ( <html lang="en"> <body>{children}</body> </html> ); }
Using window.onunhandledrejection
javascriptuseEffect(() => { const handleUnhandledRejection = (event: PromiseRejectionEvent) => { console.error('Unhandled promise rejection:', event.reason); // Send to error tracking service logErrorToService({ type: 'unhandledRejection', reason: event.reason, }); // Prevent default console error event.preventDefault(); }; window.addEventListener('unhandledrejection', handleUnhandledRejection); return () => { window.removeEventListener('unhandledrejection', handleUnhandledRejection); }; }, []);
Error Logging and Monitoring
Using Sentry
javascript// sentry.client.config.js import * as Sentry from '@sentry/nextjs'; Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, environment: process.env.NODE_ENV, tracesSampleRate: 1.0, replaysSessionSampleRate: 0.1, replaysOnErrorSampleRate: 1.0, }); // sentry.server.config.js import * as Sentry from '@sentry/nextjs'; Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, environment: process.env.NODE_ENV, tracesSampleRate: 1.0, });
Custom Error Logging
javascript// lib/errorLogger.js export function logError(error: Error, context?: any) { const errorData = { message: error.message, stack: error.stack, timestamp: new Date().toISOString(), context, }; // Send to logging service if (typeof window !== 'undefined') { // Client-side fetch('/api/log-error', { method: 'POST', body: JSON.stringify(errorData), }); } else { // Server-side console.error('Server error:', errorData); } } export function logApiError(error: any, request: Request) { const errorData = { url: request.url, method: request.method, error: error.message, stack: error.stack, timestamp: new Date().toISOString(), }; console.error('API error:', errorData); }
Best Practices
1. Provide Helpful Error Messages
javascript// ✅ Good practice export default function Error({ error, reset }) { return ( <div> <h2>Something went wrong</h2> <p>We couldn't load the page. Please try again.</p> <button onClick={reset}>Try again</button> <Link href="/">Go Home</Link> </div> ); } // ❌ Bad practice export default function Error({ error }) { return ( <div> <p>{error.message}</p> </div> ); }
2. Log Error Context
javascript// ✅ Good practice try { const user = await getUser(userId); const posts = await getPosts(user.id); return { user, posts }; } catch (error) { logError(error, { userId, action: 'fetchUserPosts' }); throw error; } // ❌ Bad practice try { const user = await getUser(userId); const posts = await getPosts(user.id); return { user, posts }; } catch (error) { console.error(error); throw error; }
3. Provide Recovery Options
javascript// ✅ Good practice export default function Error({ error, reset }) { return ( <div> <h2>Something went wrong</h2> <button onClick={reset}>Try again</button> <button onClick={() => window.location.href = '/'}> Go Home </button> </div> ); } // ❌ Bad practice export default function Error({ error }) { return ( <div> <h2>Something went wrong</h2> <p>Please refresh the page</p> </div> ); }
4. Distinguish Development and Production
javascriptexport default function Error({ error, reset }) { const isDev = process.env.NODE_ENV === 'development'; return ( <div> <h2>Something went wrong</h2> {isDev ? ( <details> <summary>Error Details</summary> <pre>{error.message}</pre> <pre>{error.stack}</pre> </details> ) : ( <p>We're sorry for the inconvenience. Please try again.</p> )} <button onClick={reset}>Try again</button> </div> ); }
5. Use Type-safe Error Handling
javascript// types/errors.ts export class AppError extends Error { constructor( message: string, public statusCode: number = 500, public code?: string ) { super(message); this.name = 'AppError'; } } export class NotFoundError extends AppError { constructor(message: string = 'Resource not found') { super(message, 404, 'NOT_FOUND'); } } export class ValidationError extends AppError { constructor(message: string = 'Validation failed') { super(message, 400, 'VALIDATION_ERROR'); } } // Usage export async function getProduct(id: string) { const product = await db.product.findUnique({ where: { id } }); if (!product) { throw new NotFoundError(`Product ${id} not found`); } return product; }
By properly using these error handling mechanisms, you can build robust and reliable Next.js applications that provide excellent user experience.