error.js
Catches runtime errors in a route segment and renders recoverable fallback UI in their place.
'use client'
export default function Error({
error,
retry,
}: {
error: Error & { digest?: string }
retry: () => void
}): React.ReactNode- Added in Next.js v13.0.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$error | Error & { digest?: string } | The thrown error, forwarded to the client. Errors from Client Components keep their original message; errors from Server Components are replaced with a generic message in production to avoid leaking details. |
$error.digestoptional | string | undefined | An automatically generated hash of the thrown error, used to match the entry in your server-side logs. |
$retry | () => void | Re-fetches and re-renders the boundary's children. If the render succeeds, the fallback is replaced by the real UI. Stable since v16.3.0. |
$resetoptional | () => void | Clears the error state and re-renders the children without re-fetching. Prefer retry() unless you specifically want to skip the re-fetch. |
Return value
React.ReactNodeThe fallback UI shown in place of the segment while the error boundary is active.
Errors
Throwing while rendering the error component itself bubbles the error up to the parent error boundary — a deliberate escape hatch.
Changelog
- PHP v16.3.0The retry prop became stable and is now the recommended recovery function; reset remains for the narrower case of re-rendering without re-fetching.
- PHP v16.2.0unstable_retry prop added.
- PHP v15.2.0global-error is now also displayed in development.
- PHP v13.1.0global-error introduced.
- PHP v13.0.0error introduced.
An error file wraps its segment in a React error boundary. When a render throws anywhere inside, this component replaces the subtree instead of the whole app going blank. In the component hierarchy it wraps loading.js, not-found.js, page.js and nested layouts — but not the layout.js or template.js in its own segment, which is exactly why a broken root layout needs global-error.js.
Put one at the top of any section whose failure should stay contained: a dashboard panel breaking should not take down the navigation.
What trips people up
- It must start with
'use client'. Error boundaries are a client-side React feature; a Server Component here fails outright. - The message differs in production. Errors thrown in Server Components arrive as a generic message plus
error.digest— log the digest and match it against your server logs. Only development shows the original text. - It cannot catch errors in its own segment's layout. Move the boundary up a segment, or use
global-error.jsfor the root. - Use
retry(), notreset().reset()re-renders without re-fetching, so a failed data load usually fails again immediately. - Because
metadatais not supported in Client Components, use React's<title>if you need one.
Example
'use client' // Error boundaries must be Client Components
import { useEffect } from 'react'
export default function Error({
error,
retry,
}: {
error: Error & { digest?: string }
retry: () => void
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error)
}, [error])
return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// Attempt to recover by re-fetching and re-rendering the segment
() => retry()
}
>
Try again
</button>
</div>
)
}When to use it
- Contain a failing dashboard widget so the sidebar and navigation stay usable
- Offer a Try again button for a flaky upstream API call
- Report the error digest to Sentry or another reporting service on mount
- Show a friendly message while logging the real server-side cause out of sight
Frequently asked questions
Why does error.js show a generic message instead of my real error?
In production, errors thrown in Server Components are stripped before being forwarded to the client so server-side details cannot leak into the browser. You get a generic message plus error.digest, a hash that matches the corresponding entry in your server logs. Errors thrown in Client Components keep their original message, and development shows the real message for both.
Why does my error.js not catch errors from the layout?
Because error.js wraps loading.js, not-found.js, page.js and nested layouts, but not the layout.js or template.js in the same segment — the boundary is rendered inside that layout. Add an error.js in the parent segment to catch it, or use global-error.js when the failure is in the root layout.
What is the difference between retry and reset in error.js?
retry() re-fetches and re-renders the boundary's children, which is what you want for a transient failure such as a flaky API; it became stable in v16.3.0. reset() only clears the error state and re-renders without re-fetching, so stale data usually reproduces the same error. Use retry() unless you have a specific reason not to re-fetch.
More examples
Surfacing the digest so support can find the log line
In production, error.message from a Server Component is deliberately generic, so the digest is the only thing tying what the user sees to the stack trace on the server. Printing it lets a user quote a reference that matches a log line exactly.
'use client'
import { useEffect } from 'react'
import { reportError } from '@/app/lib/reporting'
export default function Error({
error,
retry,
}: {
error: Error & { digest?: string }
retry: () => void
}) {
useEffect(() => {
reportError(error)
}, [error])
return (
<section>
<h2>This report could not be loaded.</h2>
{error.digest ? <p>Reference: {error.digest}</p> : null}
<button onClick={() => retry()}>Try again</button>
</section>
)
}Letting some errors bubble to a parent boundary
Throwing while rendering the error component is the documented way to escalate. The segment boundary handles ordinary failures locally, while a session failure propagates to the next boundary up, which can render a sign-in prompt for the whole page instead of one broken panel.
'use client'
export default function Error({
error,
retry,
}: {
error: Error & { digest?: string }
retry: () => void
}) {
// Session failures are not recoverable here — hand them to the parent.
if (error.message.includes('SESSION_EXPIRED')) {
throw error
}
return (
<div>
<h2>Could not load this panel.</h2>
<button onClick={() => retry()}>Try again</button>
</div>
)
}Where the boundary does and does not apply
error.js does not wrap the layout sitting in the same folder, so a throw in getNavigation() skips app/dashboard/error.tsx and lands on the boundary in the parent segment. To catch it locally, add app/error.tsx one level up — or move the fetch into the page.
// app/dashboard/layout.tsx — NOT covered by app/dashboard/error.tsx
export default async function Layout({
children,
}: {
children: React.ReactNode
}) {
const nav = await getNavigation() // a throw here escapes this segment
return (
<div>
<aside>{nav.title}</aside>
{children}
</div>
)
}
Discussion