Next.js Error Handling — error.tsx and not-found.tsx
Advertisement
Next.js Error Handling — error.tsx and not-found.tsx
Next.js provides built-in error handling with error.tsx and not-found.tsx files for graceful error management and user-friendly error pages.
- Next.js Error Handling — error.tsx and not-found.tsx
- Creating Error Boundaries
- Error Types Caught
- Layout Error Boundaries
- 404 Pages
- Triggering not-found
- Nested Error Boundaries
- Real-World Error Handling
- Handling Async Errors in Server Actions
- Error Recovery Patterns
- Testing Error Boundaries
- FAQ
Creating Error Boundaries
An error.tsx file creates an error boundary for its route segment:
// app/blog/error.tsx
'use client'
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error
reset: () => void
}) {
useEffect(() => {
// Log the error to monitoring service
console.error('Blog error:', error)
}, [error])
return (
<div className="space-y-4">
<h1>Something went wrong!</h1>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
)
}
When an error occurs in /blog or its children, this component renders.
Error Types Caught
Error boundaries catch:
- Runtime errors in Server Components
- Errors in async functions
- Errors in Client Components
They don't catch:
- Errors in layout.tsx (use layout error boundary)
- Errors in middleware
- Server-side rendering issues
Layout Error Boundaries
Create separate error boundaries for layouts:
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<body>{children}</body>
</html>
)
}
// app/global-error.tsx
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<html>
<body>
<h1>Critical Error</h1>
<p>{error.message}</p>
<button onClick={reset}>Reset</button>
</body>
</html>
)
}
global-error.tsx catches errors in the root layout.
404 Pages
Create a not-found.tsx for missing pages:
// app/not-found.tsx
import Link from 'next/link'
export default function NotFound() {
return (
<div className="text-center py-12">
<h1 className="text-4xl font-bold">404</h1>
<p className="text-xl">Page not found</p>
<Link href="/" className="text-blue-600">
Return home
</Link>
</div>
)
}
Triggering not-found
Use the notFound() function to trigger the 404 page:
// app/blog/[slug]/page.tsx
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 }) {
const post = await getPost(params.slug)
if (!post) {
notFound() // Shows app/not-found.tsx or nearest not-found.tsx
}
return <article>{post.content}</article>
}
Nested Error Boundaries
Create error boundaries at multiple levels:
// app/layout.tsx
export default function RootLayout({ children }) {
return <html>{children}</html>
}
// app/error.tsx
'use client'
export default function AppError({ error, reset }) {
return (
<div>
<h1>App Error</h1>
<button onClick={reset}>Retry</button>
</div>
)
}
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }) {
return <div className="dashboard">{children}</div>
}
// app/dashboard/error.tsx
'use client'
export default function DashboardError({ error, reset }) {
return (
<div>
<h1>Dashboard Error</h1>
<p>{error.message}</p>
<button onClick={reset}>Retry</button>
</div>
)
}
Errors in the dashboard route trigger the dashboard error boundary, not the app-level one.
Real-World Error Handling
// app/api-client/error.tsx
'use client'
import { useEffect } from 'react'
import { captureException } from '@sentry/nextjs'
export default function Error({ error, reset }) {
useEffect(() => {
// Send to error tracking service
captureException(error)
// Log to console in development
if (process.env.NODE_ENV === 'development') {
console.error(error)
}
}, [error])
return (
<div className="border-l-4 border-red-500 bg-red-50 p-4">
<h2 className="font-bold">Error Loading Data</h2>
<p className="text-sm">{error.message}</p>
<button
onClick={reset}
className="mt-4 px-4 py-2 bg-red-600 text-white rounded"
>
Try Again
</button>
</div>
)
}
Handling Async Errors in Server Actions
Server Actions automatically handle errors:
// app/actions.ts
'use server'
export async function createPost(title: string, content: string) {
try {
const post = await db.posts.create({ data: { title, content } })
return { success: true, post }
} catch (error) {
return { success: false, error: 'Failed to create post' }
}
}
// app/blog/new/page.tsx
'use client'
import { createPost } from '@/app/actions'
import { useState } from 'react'
export default function NewPostPage() {
const [error, setError] = useState('')
async function handleSubmit(formData: FormData) {
setError('')
const result = await createPost(
formData.get('title') as string,
formData.get('content') as string
)
if (!result.success) {
setError(result.error)
}
}
return (
<form action={handleSubmit}>
{error && <p className="text-red-600">{error}</p>}
<input name="title" required />
<textarea name="content" required />
<button type="submit">Create</button>
</form>
)
}
Error Recovery Patterns
Pattern 1: Automatic Retry
'use client'
import { useState, useTransition } from 'react'
export function DataFetcher() {
const [retryCount, setRetryCount] = useState(0)
const [isPending, startTransition] = useTransition()
const handleRetry = () => {
if (retryCount < 3) {
setRetryCount(prev => prev + 1)
startTransition(() => {
// Retry logic
})
}
}
return (
<div>
<p>Attempt {retryCount}</p>
<button onClick={handleRetry} disabled={isPending}>
Retry
</button>
</div>
)
}
Pattern 2: Fallback Content
export default async function DataSection() {
try {
const data = await fetchData()
return <div>{data}</div>
} catch (error) {
return <div>Using cached data</div>
}
}
Testing Error Boundaries
// components/error-boundary.test.tsx
import { render } from '@testing-library/react'
import ErrorBoundary from './error-boundary'
test('displays error message', () => {
const { getByText } = render(
<ErrorBoundary>
<ComponentThatThrows />
</ErrorBoundary>
)
expect(getByText(/something went wrong/i)).toBeInTheDocument()
})
FAQ
Q: Do error boundaries catch all errors? A: No, they catch rendering errors. Errors in event handlers need try-catch blocks.
Q: Can I customize the 404 page per route? A: Yes, create not-found.tsx at any level. Nearest not-found.tsx wins.
Q: Should I always create error.tsx? A: Create it for segments where errors are likely or need custom handling. Generic errors fall back to parent boundaries.
Proper error handling creates resilient, user-friendly applications.
Advertisement