Error handling in Next.js becomes much easier when you separate expected errors from unexpected errors.
These are problems that can happen normally, like failed requests or form validation. Instead of throw, return an error and handle it in the UI.
if (!res.ok) {return { message: 'Failed to create post' }}
notFound()When the resource doesn’t exist, use notFound():
if (!post) {notFound()}
Then add not-found.tsx to render your 404 page.
This is where something actually breaks — usually a bug or an unexpected failure. Throw the error and let an Error Boundary handle it.
throw new Error('Something went wrong')
Create an error.tsx inside the route segment:
'use client'export default function ErrorPage({error,retry,}: {error: Errorretry: () => void}) {return (<><h2>Something went wrong!</h2><button onClick={retry}>Try again</button></>)}
Errors are caught by the nearest error boundary, so you can have different error.tsx files for different parts of your app.
Error boundaries don’t catch errors inside normal event handlers like onClick. Handle those yourself with try/catch and component state. Errors thrown inside startTransition, however, can bubble to the nearest error boundary.
For smaller parts of the UI, Next.js also provides catchError() to create a custom error boundary. And for errors in the root layout, there’s global-error.tsx, which must render its own <html> and <body>.
Expected error → return the errorMissing resource → notFound()Unexpected crash → throw → error.tsxRoot layout → global-error.tsx