Error Handling & Error Boundaries
Introduction to Error Handling
Robust error handling is essential for production applications. Next.js 13+ provides a comprehensive error handling system using special files like error.tsx, global-error.tsx, and not-found.tsx. This lesson covers implementing resilient error handling throughout your application.
Understanding Error Files
Next.js uses a file-based error handling system with specific naming conventions:
- error.tsx: Catches errors in the route segment and nested children
- global-error.tsx: Catches errors in the root layout (rare but critical)
- not-found.tsx: Renders when a route or resource is not found
- loading.tsx: Shows while content is loading (covered in previous lessons)
Creating an Error Boundary
Create an error.tsx file to catch errors within a route segment:
<!-- app/error.tsx -->
'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 boundary caught:', error)
}, [error])
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8 text-center">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<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>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
Something went wrong!
</h2>
<p className="text-gray-600 mb-6">
We apologize for the inconvenience. An error occurred while processing your request.
</p>
{process.env.NODE_ENV === 'development' && (
<div className="mb-6 p-4 bg-gray-100 rounded-lg text-left">
<p className="text-sm font-mono text-red-600 break-all">
{error.message}
</p>
{error.digest && (
<p className="text-xs text-gray-500 mt-2">
Error ID: {error.digest}
</p>
)}
</div>
)}
<button
onClick={reset}
className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Try Again
</button>
<a
href="/"
className="block mt-4 text-sm text-blue-600 hover:text-blue-700"
>
Return to Home
</a>
</div>
</div>
)
}
Global Error Handler
Create global-error.tsx to catch errors in the root layout:
<!-- app/global-error.tsx -->
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<div className="min-h-screen flex items-center justify-center bg-gray-900 text-white px-4">
<div className="max-w-md w-full text-center">
<h1 className="text-4xl font-bold mb-4">Critical Error</h1>
<p className="text-gray-400 mb-8">
A critical error occurred. Please refresh the page or contact support.
</p>
{process.env.NODE_ENV === 'development' && (
<div className="mb-6 p-4 bg-red-900 rounded-lg text-left">
<p className="text-sm font-mono break-all">
{error.message}
</p>
</div>
)}
<button
onClick={reset}
className="px-6 py-3 bg-white text-gray-900 rounded-lg hover:bg-gray-100"
>
Try Again
</button>
</div>
</div>
</body>
</html>
)
}
Not Found Pages
Create custom 404 pages using not-found.tsx:
<!-- app/not-found.tsx -->
import Link from 'next/link'
export default function NotFound() {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 px-4">
<div className="max-w-lg w-full text-center">
<h1 className="text-9xl font-bold text-blue-600 mb-4">404</h1>
<h2 className="text-3xl font-bold text-gray-900 mb-4">
Page Not Found
</h2>
<p className="text-gray-600 mb-8">
Sorry, we couldn't find the page you're looking for.
It might have been moved or deleted.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
href="/"
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Go to Home
</Link>
<button
onClick={() => window.history.back()}
className="px-6 py-3 bg-white text-gray-900 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
Go Back
</button>
</div>
<div className="mt-12">
<p className="text-sm text-gray-500 mb-4">Popular Pages:</p>
<div className="flex flex-wrap justify-center gap-2">
<Link href="/about" className="text-sm text-blue-600 hover:underline">
About
</Link>
<span className="text-gray-300">•</span>
<Link href="/blog" className="text-sm text-blue-600 hover:underline">
Blog
</Link>
<span className="text-gray-300">•</span>
<Link href="/contact" className="text-sm text-blue-600 hover:underline">
Contact
</Link>
</div>
</div>
</div>
</div>
)
}
Programmatic Not Found
Trigger the not-found page programmatically using notFound():
<!-- app/posts/[slug]/page.tsx -->
import { notFound } from 'next/navigation'
import { prisma } from '@/lib/prisma'
interface PageProps {
params: {
slug: string
}
}
export default async function PostPage({ params }: PageProps) {
const post = await prisma.post.findUnique({
where: { slug: params.slug }
})
if (!post) {
notFound()
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
Route-Specific Error Pages
Create error pages for specific route segments:
<!-- app/dashboard/error.tsx -->
'use client'
import { useRouter } from 'next/navigation'
export default function DashboardError({
error,
reset,
}: {
error: Error
reset: () => void
}) {
const router = useRouter()
return (
<div className="p-8 max-w-lg mx-auto">
<h2 className="text-2xl font-bold text-red-600 mb-4">
Dashboard Error
</h2>
<p className="text-gray-600 mb-6">
An error occurred while loading your dashboard. This could be due to a
network issue or a temporary problem with our servers.
</p>
<div className="flex gap-4">
<button
onClick={reset}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Retry
</button>
<button
onClick={() => router.push('/')}
className="px-4 py-2 bg-gray-200 text-gray-900 rounded-lg hover:bg-gray-300"
>
Go Home
</button>
</div>
</div>
)
}
<!-- app/dashboard/not-found.tsx -->
export default function DashboardNotFound() {
return (
<div className="p-8 text-center">
<h2 className="text-2xl font-bold mb-4">Dashboard Page Not Found</h2>
<p className="text-gray-600">
The dashboard page you're looking for doesn't exist.
</p>
</div>
)
}
Error Recovery Patterns
Implement intelligent error recovery strategies:
<!-- app/components/error-boundary-wrapper.tsx -->
'use client'
import { Component, ReactNode } from 'react'
interface Props {
children: ReactNode
fallback: (error: Error, retry: () => void) => ReactNode
}
interface State {
hasError: boolean
error: Error | null
errorCount: number
}
export class ErrorBoundaryWrapper extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
hasError: false,
error: null,
errorCount: 0
}
}
static getDerivedStateFromError(error: Error): Partial<State> {
return {
hasError: true,
error,
}
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error boundary caught:', error, errorInfo)
// Send to error reporting service
// reportError(error, errorInfo)
this.setState(prevState => ({
errorCount: prevState.errorCount + 1
}))
}
retry = () => {
// Limit retry attempts
if (this.state.errorCount < 3) {
this.setState({
hasError: false,
error: null,
})
} else {
alert('Maximum retry attempts reached. Please refresh the page.')
}
}
render() {
if (this.state.hasError && this.state.error) {
return this.props.fallback(this.state.error, this.retry)
}
return this.props.children
}
}
API Error Handling
Handle errors in API routes and Server Actions:
<!-- app/api/posts/route.ts -->
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { z } from 'zod'
const createPostSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
})
export async function POST(request: Request) {
try {
const body = await request.json()
// Validate input
const validatedData = createPostSchema.parse(body)
// Create post
const post = await prisma.post.create({
data: validatedData
})
return NextResponse.json(post, { status: 201 })
} catch (error) {
console.error('Create post error:', error)
// Validation error
if (error instanceof z.ZodError) {
return NextResponse.json(
{
error: 'Validation failed',
issues: error.issues
},
{ status: 400 }
)
}
// Database error
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
return NextResponse.json(
{ error: 'A post with this title already exists' },
{ status: 409 }
)
}
}
// Generic error
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
Server Action Error Handling
<!-- app/actions/post-actions.ts -->
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { prisma } from '@/lib/prisma'
import { z } from 'zod'
const postSchema = z.object({
title: z.string().min(1, 'Title is required'),
content: z.string().min(10, 'Content must be at least 10 characters'),
})
export async function createPost(prevState: any, formData: FormData) {
try {
const rawData = {
title: formData.get('title'),
content: formData.get('content'),
}
// Validate
const validatedData = postSchema.parse(rawData)
// Create post
const post = await prisma.post.create({
data: validatedData
})
// Revalidate and redirect
revalidatePath('/posts')
redirect(`/posts/${post.slug}`)
} catch (error) {
console.error('Create post error:', error)
if (error instanceof z.ZodError) {
return {
success: false,
errors: error.flatten().fieldErrors,
message: 'Validation failed'
}
}
return {
success: false,
message: 'Failed to create post. Please try again.'
}
}
}
Client-Side Error Handling
<!-- app/components/safe-component.tsx -->
'use client'
import { useState, useEffect } from 'react'
export function SafeComponent() {
const [data, setData] = useState(null)
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
fetchData()
}, [])
const fetchData = async () => {
try {
setIsLoading(true)
setError(null)
const response = await fetch('/api/data')
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result = await response.json()
setData(result)
} catch (err) {
console.error('Fetch error:', err)
setError(err instanceof Error ? err.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
if (isLoading) {
return <div>Loading...</div>
}
if (error) {
return (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
<h3 className="text-red-800 font-semibold mb-2">Error</h3>
<p className="text-red-600 mb-4">{error}</p>
<button
onClick={fetchData}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Retry
</button>
</div>
)
}
return <div>{/* Render data */}</div>
}
- Create custom error pages for different route segments (blog, dashboard, profile)
- Implement error logging to a service like Sentry or LogRocket
- Add retry logic with exponential backoff for failed API requests
- Create a custom error boundary that shows different UI based on error type
- Implement graceful degradation for non-critical features that fail
- Add user-friendly error messages for common error scenarios
Error Handling Best Practices
- Always provide a way to recover from errors (retry button, navigation links)
- Show different error messages in development vs production
- Log errors to monitoring services for debugging
- Use specific error pages for different route segments
- Provide helpful context and next steps in error messages
- Validate input early to catch errors before they propagate
- Use proper HTTP status codes in API responses
- Implement fallback UI for graceful degradation
- Never expose sensitive error details to users in production
Summary
Effective error handling in Next.js uses error.tsx, global-error.tsx, and not-found.tsx files to create resilient applications. Always provide recovery mechanisms, log errors properly, show user-friendly messages, and implement graceful degradation. Good error handling improves user experience and makes debugging easier.