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.