Drop a loading.tsx in a route segment to wrap its page.tsx in a Suspense boundary automatically.
1// app/dashboard/loading.tsx
2export default function Loading() {
3 return (
4 <div className="space-y-3 p-8">
5 <div className="h-8 w-1/3 rounded bg-gray-200 animate-pulse" />
6 <div className="h-32 w-full rounded bg-gray-200 animate-pulse" />
7 </div>
8 );
9}While the server renders page.tsx, the browser sees the skeleton immediately.
app/
dashboard/loading.tsx ← shown on /dashboard, /dashboard/*
dashboard/billing/loading.tsx ← takes over inside billing
A nested loading state replaces the parent's only for its own subtree.
1// app/dashboard/error.tsx
2"use client";
3
4export default function DashboardError({
5 error,
6 reset,
7}: {
8 error: Error & { digest?: string };
9 reset: () => void;
10}) {
11 return (
12 <div className="p-8 text-center">
13 <h2 className="text-xl font-bold">Something went wrong</h2>
14 <p className="text-sm text-gray-600">Error code: {error.digest}</p>
15 <button onClick={reset} className="mt-4 rounded bg-black px-4 py-2 text-white">
16 Try again
17 </button>
18 </div>
19 );
20}error.tsx must be a Client Component (it uses React's error boundary mechanism). The reset() function re-renders the segment.
The error catcher resets independently. A failure in /dashboard/billing doesn't blank out the dashboard chrome — only the billing region shows the error UI.
1// app/global-error.tsx
2"use client";
3
4export default function GlobalError({ error, reset }: ErrorProps) {
5 return (
6 <html>
7 <body>
8 <h2>Application crashed</h2>
9 <button onClick={reset}>Retry</button>
10 </body>
11 </html>
12 );
13}This file is mandatory to render when even the root layout fails — that's why it includes its own <html><body>.
Render when you call notFound() or hit an unmatched URL.
1// app/blog/[slug]/page.tsx
2import { notFound } from "next/navigation";
3
4const post = await db.post.findUnique({ where: { slug } });
5if (!post) notFound(); // unwinds, renders the nearest not-found.tsx1// app/blog/[slug]/not-found.tsx
2export default function NotFound() {
3 return (
4 <div className="p-8 text-center">
5 <h1 className="text-3xl font-bold">Post not found</h1>
6 <Link href="/blog">← Back to blog</Link>
7 </div>
8 );
9}1export default async function Page() {
2 const data = await fetch("/api/something");
3 if (!data.ok) throw new Error("Upstream failure");
4 // … nearest error.tsx renders
5}In production the error message is redacted from the client and replaced with a unique error.digest. Use the digest to look up the full trace in your server logs.
app/dashboard/
layout.tsx
loading.tsx ← shows skeleton while page streams
error.tsx ← catches errors in the dashboard tree
not-found.tsx ← shown if notFound() is called inside
page.tsx
billing/
loading.tsx
error.tsx
page.tsx
This is the production-grade default. Build it once, never think about generic loading/error screens again.