Learn how to handle expected errors with return values and unexpected exceptions with error boundaries in Next.js App Router.
Errors fall into two categories: expected errors that can occur during normal operation (like form validation failures), and unexpected exceptions that indicate bugs. This page covers how to handle both.
In a Server Component, use the fetch response to conditionally render an error message:
export default async function Page() { const res = await fetch('https://...') const data = await res.json() if (!res.ok) { return 'There was an error.' } return '...'}
Create an error.tsx file inside a route segment to define an error boundary for that segment:
'use client' // Error boundaries must be Client Componentsimport { useEffect } from 'react'export default function ErrorPage({ error, unstable_retry,}: { error: Error & { digest?: string } unstable_retry: () => void}) { useEffect(() => { console.error(error) }, [error]) return ( <div> <h2>Something went wrong!</h2> <button onClick={() => unstable_retry()}> Try again </button> </div> )}
Errors bubble up to the nearest parent error boundary. Place error.tsx files at different levels in the route hierarchy to control the scope of error recovery.Component hierarchy with error boundary:
app/dashboard/├── layout.tsx├── error.tsx ← catches errors from page.tsx└── page.tsx
The error.tsx component must be a Client Component because it uses React lifecycle methods to catch and display errors during rendering.
Error boundaries don’t catch errors inside event handlers — they only catch errors during rendering. For event handler errors, use try/catch with useState:
'use client'import { useState } from 'react'export function Button() { const [error, setError] = useState<Error | null>(null) const handleClick = () => { try { throw new Error('Exception') } catch (reason) { setError(reason as Error) } } if (error) { return <p>Error: {error.message}</p> } return ( <button type="button" onClick={handleClick}> Click me </button> )}