Errors, 404s and Redirects
IntermediateFour files and two functions cover every failure path. The subtlety is that notFound() and redirect() work by throwing, which interacts badly with try/catch.
Overview
The framework gives you the failure UI by convention — an error boundary per segment, a 404 page per segment, and a global catch for the root layout — so the work is deciding what each one says and where the boundaries go. The one piece of real mechanics is control flow: `notFound()` and `redirect()` are implemented as thrown exceptions that Next catches, which means a `try/catch` around your data fetching will swallow them and turn a redirect into a silent no-op. That bug is common and confusing, and knowing it in advance saves an afternoon.
The Files
Each one is a boundary, and placement decides how much survives.
app/
error.tsx // catches this segment and below ('use client')
not-found.tsx // rendered by notFound(), and for unmatched URLs
global-error.tsx // catches errors in the ROOT layout itself
problems/
error.tsx // a failure here keeps the site header
// error.tsx — the reset function re-renders the segment
'use client'
export default function Error({ error, reset }: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => { logToSentry(error) }, [error])
return (
<div role="alert" className="p-8 text-center">
<h2 className="font-semibold">We could not load this.</h2>
<p className="text-sm text-muted-foreground">
{error.digest && `Reference: ${error.digest}`}
</p>
<button onClick={reset}>Try again</button>
<Link href="/">Go home</Link>
</div>
)
}
// In production, error.message is redacted to a generic string and a
// digest is provided instead — the digest matches a line in your
// server logs. Show it, so a support message is traceable.
// global-error.tsx must render its own <html> and <body>, because the
// root layout is what failed.notFound and redirect Throw
The gotcha that produces a silent failure.
import { notFound, redirect } from 'next/navigation'
// Normal use — no return needed, because they throw
const problem = await getProblem(slug)
if (!problem) notFound() // renders the nearest not-found.tsx
// AND sends a real 404 status
if (!user) redirect('/login') // 307 by default
// The bug:
try {
const problem = await getProblem(slug)
if (!problem) notFound() // throws...
} catch (e) {
return <ErrorState /> // ...and your catch eats it
}
// The user gets a generic error instead of a 404, and the status code
// is 200. Search engines then index the error page.
// Fix — call them outside the try, or rethrow:
let problem
try { problem = await getProblem(slug) }
catch { return <ErrorState /> }
if (!problem) notFound()
// The status code matters beyond correctness: a soft 404 (a "not
// found" page returning 200) gets indexed, which is why notFound()
// exists rather than just rendering a component.
// permanentRedirect() sends 308 for a moved URL — use it for real
// moves so search engines update, and prefer next.config redirects
// for static ones so they never hit the app.Redirects in Config
The cheapest place to handle a moved URL.
// next.config.js — handled before the app runs
async redirects() {
return [
{ source: '/old-problems', destination: '/dsa/problems', permanent: true },
{ source: '/blog/:slug', destination: '/how/:slug', permanent: true },
]
}
// permanent: true -> 308, and search engines transfer ranking.
// permanent: false -> 307, for a temporary move.
// rewrites keep the URL and serve different content — a proxy:
async rewrites() {
return [{ source: '/api/:path*', destination: `${process.env.API_URL}/:path*` }]
}
// Where to put a redirect, cheapest first:
// next.config static, known at build time
// middleware depends on the request (auth, locale, geo)
// in the page depends on data you had to fetch anyway
// And distinguish the two 404s in your copy, because they need
// different words:
// the ROUTE does not exist -> "Page not found", offer navigation
// the route exists, the RECORD does not
// -> "This problem was removed", keep the
// surrounding layout and suggest othersKey Points to Remember
- 1error.tsx is a client component that catches its segment and below; global-error.tsx covers a failing root layout
- 2Production redacts error.message and gives a digest instead — show it so support can match a log line
- 3notFound() and redirect() throw, so calling them inside a try/catch silently swallows them
- 4A "not found" page that returns 200 is a soft 404 and gets indexed; notFound() sends a real 404
- 5Handle static redirects in next.config, request-dependent ones in middleware, and data-dependent ones in the page
Interview Questions
Sign in to ask AriaWhy does calling notFound() inside a try/catch break?
What is a soft 404 and why does it matter?
Where should a permanent redirect for a moved URL live?
Ask Aria about Errors, 404s and Redirects
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.