Home/Learn/Next.js/Errors, 404s and Redirects

Errors, 404s and Redirects

Intermediate
UX & Assets

Four 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.

error.tsx, not-found.tsx, global-error.tsx
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.

Never inside a try/catch
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.

Config, middleware, or the page — in that order
// 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 others

Key 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 Aria
1

Why does calling notFound() inside a try/catch break?

Hard
2

What is a soft 404 and why does it matter?

Medium
3

Where should a permanent redirect for a moved URL live?

Medium

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.

Loading discussion…