Not Found & Errors
A user clicks a shared link to a blog post that was deleted last week. They see a blank screen. Or worse: a raw stack trace with your database connection string. Error handling isn't about catching exceptions—it's about what your users see when things go wrong.
Clear failure UIs prevent confusion and reduce support tickets. Error boundaries isolate failures and provide contextual recovery options.
Outcome
A demo showing notFound() routing to the nearest not-found.tsx and nested error boundaries with recovery.
Fast Track
- Add
not-found.tsxat root and nested segment. - Call
notFound()in a page/data path. - Verify error surface precedence with nested
error.tsx.
Hands-On Exercise 2.3
Build error handling surfaces with proper boundary hierarchy.
Requirements:
- Show 404 handling for missing slugs with
notFound(). - Add a nested
error.tsxboundary that overrides root. - Include a reset action on error boundary for recovery.
- Add correlation IDs (unique identifiers for tracking requests across systems) for debugging.
Implementation hints:
- notFound() function: Triggers nearest
not-found.tsxfile up the tree, stops component rendering, sets HTTP 404 status. - Error boundary hierarchy: Nearest
error.tsxcatches runtime errors, allows different error UIs for different sections. - Reset function: Provides user recovery option to retry failed operation.
- Debugging: Include correlation IDs in error logs to trace issues.
- Different error UIs: Show contextual messages (e.g., "Dashboard Unavailable" vs generic "Application Error").
- Keep error boundary as a Client Component (must use
'use client'). - Return early with
notFound()where appropriate (e.g., when blog post not found). - Don't catch
notFound()in try/catch blocks—it works by throwing an error.
All error.tsx files must be Client Components. Add 'use client' at the top of the file.
notFound() stops execution immediately by throwing an error. Don't catch it in try/catch blocks or it won't trigger the not-found.tsx UI.
Try It
- Navigate to a missing resource; confirm nearest not-found UI.
Commit & Deploy
git add -A
git commit -m "feat(core): implement notFound() + nested error surfaces"
git push -u origin feat/core-notfound-errorDone-When
- Navigate to
/posts/nonexistent-slug: nested "Post Not Found" page appears (not generic 404) - Navigate to
/random-nonexistent-page: root "404" page appears - Navigate to
/error-demo, click "Trigger Error": nested error boundary with orange styling appears - On error boundary, click "Try Again": page resets to initial state without page refresh
- On error boundary, verify "Correlation ID" is displayed for debugging
Solution
Solution
We use /posts as a demo route in apps/web to practice error handling.
This is separate from the actual blog app (apps/blog) which has its own routes.
File Structure
apps/web/src/app/
├── not-found.tsx # Root 404 page (fallback)
├── error.tsx # Root error boundary
├── posts/
│ └── [slug]/
│ ├── page.tsx # Uses notFound() for missing slugs
│ └── not-found.tsx # Nested 404 (overrides root)
└── error-demo/
├── page.tsx # Throws errors to test boundary
└── error.tsx # Nested error boundary with correlation ID
Root 404 Page
// Root not-found.tsx - catches all 404s not handled by nested not-found files
import Link from "next/link";
export default function NotFound() {
return (
<div className="flex min-h-[50vh] flex-col items-center justify-center">
<h1 className="mb-4 font-bold text-4xl">404</h1>
<p className="mb-4 text-gray-600">This page doesn't exist.</p>
<Link
href="/"
className="rounded bg-gray-900 px-4 py-2 text-white hover:bg-gray-800"
>
Go home
</Link>
</div>
);
}Root Error Boundary
"use client";
// Root error boundary - catches all runtime errors not handled by nested boundaries
// Must be a Client Component: error boundaries use React state internally
import { useEffect } from "react";
// Rename to ErrorBoundary to avoid shadowing global Error
export default function ErrorBoundary({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// biome-ignore lint/suspicious/noConsole: Error logging is intentional for debugging
console.error("Root error boundary caught:", error);
}, [error]);
return (
<div className="flex min-h-[50vh] flex-col items-center justify-center">
<div className="rounded-lg border border-red-200 bg-red-50 p-8 text-center">
<h2 className="mb-2 font-bold text-2xl text-red-800">
Something went wrong
</h2>
<p className="mb-4 text-red-600">
{error.message || "An unexpected error occurred"}
</p>
{/* digest is Next.js's auto-generated error ID for production - a unique hash that correlates client errors with server logs */}
{error.digest && (
<p className="mb-4 font-mono text-red-400 text-xs">
Error ID: {error.digest}
</p>
)}
<button
type="button"
onClick={reset}
className="rounded bg-red-600 px-6 py-2 font-medium text-white hover:bg-red-700"
>
Try again
</button>
</div>
</div>
);
}Nested 404 for Posts
// Nested not-found.tsx - overrides root for /posts/[slug] routes
// Provides contextual messaging: "Post not found" instead of generic "Page not found"
import Link from "next/link";
export default function PostNotFound() {
return (
<div className="flex min-h-[50vh] flex-col items-center justify-center">
<h1 className="mb-4 font-bold text-4xl">Post Not Found</h1>
<p className="mb-4 text-gray-600">
The blog post you're looking for doesn't exist.
</p>
<p className="mb-6 text-gray-500 text-sm">
This is the nested not-found.tsx in /posts/[slug]/
</p>
<div className="flex gap-4">
<Link
href="/posts"
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
Browse Posts
</Link>
<Link
href="/"
className="rounded border border-gray-300 px-4 py-2 text-gray-700 hover:bg-gray-50"
>
Go Home
</Link>
</div>
</div>
);
}Page Using notFound()
// Demonstrates notFound() routing to the nearest not-found.tsx
import { notFound } from "next/navigation";
import Link from "next/link";
// Simulated posts database
const posts: Record<string, { title: string; content: string }> = {
"hello-world": {
title: "Hello World",
content: "This is the first post. Welcome to the blog!",
},
"nextjs-tips": {
title: "Next.js Tips",
content: "Here are some tips for building with Next.js...",
},
};
// In Next.js 16, params is a Promise that must be awaited
export default async function PostPage(props: {
params: Promise<{ slug: string }>;
}) {
const params = await props.params;
const post = posts[params.slug];
// If post doesn't exist, trigger 404
// notFound() throws - don't wrap in try/catch or it won't work
if (!post) {
notFound();
}
return (
<main className="mx-auto max-w-2xl p-8">
<Link
href="/posts"
className="mb-4 inline-block text-blue-600 hover:underline"
>
← Back to posts
</Link>
<h1 className="mb-4 font-bold text-3xl">{post.title}</h1>
<p className="text-gray-600">{post.content}</p>
</main>
);
}Nested Error Boundary with Correlation ID
"use client";
// Nested error boundary with correlation ID for debugging
// Demonstrates: different error UI, correlation tracking, reset functionality
import { useEffect, useMemo } from "react";
// Generate a correlation ID for debugging
// In production, this helps trace errors across logs and support tickets
function generateCorrelationId(): string {
return `err-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}
export default function ErrorDemoBoundary({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
// Memoize so correlation ID stays stable during re-renders
const correlationId = useMemo(() => generateCorrelationId(), []);
useEffect(() => {
// Log error with correlation ID for debugging
// In production: send to Sentry, DataDog, or your error reporting service
const errorLog = {
correlationId,
digest: error.digest,
message: error.message,
timestamp: new Date().toISOString(),
location: "/error-demo",
};
// biome-ignore lint/suspicious/noConsole: Error logging is intentional
console.error("Error demo boundary caught:", errorLog);
}, [error, correlationId]);
return (
<div className="mx-auto max-w-2xl p-8">
<div className="rounded-lg border-2 border-orange-300 bg-orange-50 p-6">
<h2 className="mb-2 font-bold text-orange-800 text-xl">
Demo Error Caught!
</h2>
<p className="mb-4 text-orange-700">
This error was caught by the nested error boundary in /error-demo.
</p>
<p className="mb-4 font-mono text-orange-600 text-sm">
{error.message}
</p>
{/* Correlation ID: give to users for support tickets */}
<p className="mb-4 font-mono text-orange-400 text-xs">
Correlation ID: {correlationId}
</p>
{error.digest && (
<p className="mb-4 font-mono text-orange-400 text-xs">
Digest: {error.digest}
</p>
)}
<div className="flex gap-3">
{/* reset() re-renders the component tree, clearing the error state */}
<button
type="button"
onClick={reset}
className="rounded bg-orange-600 px-4 py-2 text-white hover:bg-orange-700"
>
Try Again
</button>
<a
href="/error-demo"
className="rounded border border-orange-600 px-4 py-2 text-orange-600 hover:bg-orange-100"
>
Reload Page
</a>
</div>
</div>
</div>
);
}Error Demo Page (triggers the boundary)
"use client";
// Page that throws errors to test the nested error boundary
// Must be Client Component to use useState for triggering errors
import { useState } from "react";
export default function ErrorDemoPage() {
const [shouldError, setShouldError] = useState(false);
// Throwing during render triggers the nearest error.tsx
if (shouldError) {
throw new Error("This is a demo error triggered by the button!");
}
return (
<div className="mx-auto max-w-2xl p-8">
<h1 className="mb-4 font-bold text-3xl">Error Boundary Demo</h1>
<p className="mb-6 text-gray-600">
Click the button to trigger an error and see the nested error boundary.
</p>
<button
type="button"
onClick={() => setShouldError(true)}
className="rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700"
>
Trigger Error
</button>
</div>
);
}Testing
- 404 Test: Visit
/posts/nonexistent-slug→ Should show "Post Not Found" (nested) - 404 Fallback: Visit
/random-page→ Should show "404" (root) - Error Test: Visit
/error-demoand click "Trigger Error" → Shows orange error UI - Reset Test: Click "Try Again" on error boundary → Page resets to initial state
- Correlation ID: Check console for
correlationIdin error log
References
Was this helpful?