Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
14 minLesson 16 of 33
Next.js App Router

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

Get Notes Free →
!