Error Boundaries & not-found
Error Boundaries & not-found in Next.js
Production apps fail. Database connections drop, external APIs return 500s, unexpected data causes JavaScript errors. The question isn't whether errors will happen — it's whether they'll crash your entire app or be handled gracefully. Next.js gives you error.tsx and not-found.tsx to contain and handle both types of failure.
error.tsx — Catching Runtime Errors
Create an error.tsx file alongside any page.tsx to catch errors thrown during rendering or data fetching in that route:
// src/app/error.tsx — global error boundary
"use client"; // error.tsx must be a Client Component
import { useEffect } from "react";
interface ErrorProps {
error: Error & { digest?: string };
reset: () => void;
}
export default function GlobalError({ error, reset }: ErrorProps) {
useEffect(() => {
// Log to an error reporting service
console.error("Unhandled error:", error);
}, [error]);
return (
<main className="min-h-screen flex items-center justify-center px-4">
<div className="max-w-md text-center">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-6">
<svg className="w-8 h-8 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<h1 className="text-2xl font-bold text-gray-900">Something went wrong</h1>
<p className="text-gray-500 mt-2">
{process.env.NODE_ENV === "development"
? error.message
: "An unexpected error occurred. We've been notified."}
</p>
{error.digest && (
<p className="text-xs text-gray-400 mt-1">Error ID: {error.digest}</p>
)}
<div className="flex gap-3 justify-center mt-6">
<button
onClick={reset}
className="bg-blue-600 hover:bg-blue-700 text-white font-semibold px-5 py-2.5 rounded-lg transition-colors"
>
Try again
</button>
<a
href="/"
className="border border-gray-300 text-gray-700 hover:bg-gray-50 font-semibold px-5 py-2.5 rounded-lg transition-colors"
>
Go home
</a>
</div>
</div>
</main>
);
}
The reset function re-renders the error boundary and retries the failed component. The error.digest is a hash of the error for server-side log correlation.
Scoped Error Boundaries
Put error.tsx inside a specific folder to limit the error boundary to that section:
src/app/
├── error.tsx → Catches errors anywhere in the app
├── dashboard/
│ ├── error.tsx → Only catches errors in /dashboard/**
│ ├── page.tsx
│ └── analytics/
│ ├── error.tsx → Only catches errors in /dashboard/analytics
│ └── page.tsx
The analytics error boundary doesn't crash the whole dashboard. An analytics error shows a friendly error UI just for that section while the rest of the dashboard works normally.
not-found.tsx — Handling Missing Resources
When a page calls notFound(), Next.js renders the nearest not-found.tsx:
// src/app/not-found.tsx — shown for any 404
import Link from "next/link";
export default function NotFound() {
return (
<main className="min-h-screen flex items-center justify-center px-4">
<div className="max-w-md text-center">
<h1 className="text-9xl font-black text-gray-100 select-none">404</h1>
<div className="-mt-8">
<h2 className="text-3xl font-bold text-gray-900">Page not found</h2>
<p className="text-gray-500 mt-3">
The page you're looking for doesn't exist or has been moved.
</p>
<div className="flex gap-3 justify-center mt-8">
<Link
href="/"
className="bg-blue-600 hover:bg-blue-700 text-white font-semibold px-5 py-2.5 rounded-lg transition-colors"
>
Go home
</Link>
<Link
href="/courses"
className="border border-gray-300 text-gray-700 hover:bg-gray-50 font-semibold px-5 py-2.5 rounded-lg transition-colors"
>
Browse courses
</Link>
</div>
</div>
</div>
</main>
);
}
Trigger it from any server component or API route:
// src/app/courses/[slug]/page.tsx
import { notFound } from "next/navigation";
export default async function CoursePage({ params }: { params: { slug: string } }) {
const course = await getCourse(params.slug);
if (!course) {
notFound(); // renders not-found.tsx with 404 status
}
return <CourseDetail course={course} />;
}
Scoped not-found.tsx files work just like scoped error.tsx:
// src/app/courses/not-found.tsx — specific to /courses/**
export default function CourseNotFound() {
return (
<div className="text-center py-16">
<h2 className="text-2xl font-bold">Course not found</h2>
<p className="text-gray-500 mt-2">
This course doesn't exist or has been removed.
</p>
<Link href="/courses" className="mt-6 inline-block text-blue-600 hover:underline">
Browse all courses
</Link>
</div>
);
}
Triggering Errors vs Not Found
// Use notFound() for "this resource doesn't exist"
// → 404 status, renders not-found.tsx
if (!course) notFound();
// Throw an error for unexpected failures
// → 500 status, renders error.tsx
if (!db) throw new Error("Database connection failed");
// Use redirect() for auth failures
// → 307 redirect, no error boundary
if (!user) redirect("/login");
Error Handling in API Routes
// src/app/api/courses/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
try {
const course = await db.course.findUnique({ where: { id: params.id } });
if (!course) {
return NextResponse.json(
{ error: "Course not found" },
{ status: 404 }
);
}
return NextResponse.json(course);
} catch (error) {
console.error("Failed to fetch course:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
Global Error for Layout Errors
If the root layout.tsx itself throws, you need a special global-error.tsx (it replaces the entire <html> in that case):
// src/app/global-error.tsx
"use client";
export default function GlobalError({ error, reset }: { error: Error; reset: () => void }) {
return (
<html>
<body>
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1>Something went catastrophically wrong</h1>
<button onClick={reset}>Try again</button>
</div>
</div>
</body>
</html>
);
}
global-error.tsx is a last resort — layout errors are rare. The standard error.tsx handles page-level errors in the vast majority of cases.
Next lesson: Route groups and parallel routes — advanced App Router patterns for complex navigation.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises