Skip to main content
Chapter 2 App Router, Layouts, and Navigation

Loading, Error, and Not-Found States

18 min read Lesson 7 / 28

Handling Loading, Errors, and Missing Pages

The App Router provides dedicated files for every state a route can be in. These file conventions integrate with React's Suspense and Error Boundary systems automatically.

loading.tsx — Instant Loading States

Create loading.tsx in any route segment and Next.js will stream the page with this skeleton while the Server Component fetches data:

// app/blog/loading.tsx
export default function BlogLoading() {
    return (
        <div className="space-y-6">
            {Array.from({ length: 5 }).map((_, i) => (
                <div key={i} className="animate-pulse">
                    <div className="h-6 bg-gray-200 rounded w-3/4 mb-2" />
                    <div className="h-4 bg-gray-200 rounded w-1/2" />
                </div>
            ))}
        </div>
    );
}

The page beneath streams in as soon as it resolves, replacing the skeleton. The loading UI appears instantly — no layout shift.

error.tsx — Per-Segment Error Boundaries

// app/blog/error.tsx
'use client'; // Error components MUST be Client Components

import { useEffect } from 'react';

interface ErrorProps {
    error: Error & { digest?: string };
    reset: () => void;
}

export default function BlogError({ error, reset }: ErrorProps) {
    useEffect(() => {
        console.error('Blog section error:', error);
    }, [error]);

    return (
        <div className="text-center py-20">
            <h2 className="text-2xl font-bold mb-4">Something went wrong</h2>
            <p className="text-gray-500 mb-6">{error.message}</p>
            <button
                onClick={reset}
                className="px-4 py-2 bg-blue-600 text-white rounded-lg"
            >
                Try again
            </button>
        </div>
    );
}

Errors are caught at the nearest error.tsx boundary without crashing the rest of the page.

not-found.tsx — 404 Pages

// app/blog/[slug]/not-found.tsx
import Link from 'next/link';

export default function PostNotFound() {
    return (
        <div className="text-center py-20">
            <h1 className="text-4xl font-bold mb-4">Post Not Found</h1>
            <p className="text-gray-500 mb-6">
                The post you are looking for does not exist or has been removed.
            </p>
            <Link href="/blog" className="text-blue-600 hover:underline">
                Back to Blog
            </Link>
        </div>
    );
}

Trigger it from a Server Component:

import { notFound } from 'next/navigation';

const post = await getPost(slug);

if (!post) {
    notFound(); // Renders not-found.tsx
}

These three file conventions replace dozens of lines of manual conditional rendering with a clean, declarative approach.