A real application spends much of its life in states other than "everything loaded fine": data is still on its way, a request failed, or the URL points at nothing. The App Router gives each state its own file convention. After this lesson you will be able to add loading.tsx, error.tsx, global-error.tsx and not-found.tsx to any route segment and trigger them on purpose.
| File | Rendered when | Notes |
|---|---|---|
| loading.tsx | The segment's page.tsx is still fetching data | Wraps the page in a React Suspense boundary automatically |
| error.tsx | The page or a nested segment throws during render | Must be a Client Component; receives error and reset |
| global-error.tsx | The root layout itself throws | Must render its own <html> and <body> |
| not-found.tsx | notFound() is called, or no route matches (root only) | Returns HTTP 404 |
Each file applies to its own folder and everything below it.
Create app/dashboard/loading.tsx and export any component:
// app/dashboard/loading.tsx
export default function Loading() {
return <div className="skeleton" aria-busy="true">Loading dashboard...</div>;
}Behind the scenes Next.js renders <Suspense fallback={<Loading />}>{page}</Suspense>. The layout and navigation are sent to the browser immediately while the slow page streams in afterwards, and the fallback also appears during client-side navigation. Keep skeletons the same size as the final content to avoid layout shift.
error.tsx is a React Error Boundary, which is why it must be a Client Component:
// app/dashboard/error.tsx
"use client";
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<section>
<h2>Something went wrong</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</section>
);
}Calling reset() re-renders the segment, which is often enough after a transient failure. In production, Next.js strips messages of errors thrown on the server and provides a digest you can match against server logs, so do not rely on error.message for user-facing text.
An error.tsx catches errors from its sibling page.tsx and from all nested segments, but not from the layout.tsx in the same folder. To catch a layout's errors, place error.tsx one level up. The root layout is covered by app/global-error.tsx, a Client Component with the same props that must render its own <html> and <body> tags because it replaces the root layout entirely.
Two situations produce a 404. When no route matches, Next.js renders app/not-found.tsx. When a route exists but the requested item does not, call notFound() from next/navigation:
// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
import { getPost } from "@/lib/posts";
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound(); // stops rendering and shows the nearest not-found.tsx
return <article>{post.title}</article>;
}notFound() throws a special error, so code after it never runs. The nearest not-found.tsx up the tree renders with a real 404 status code, which matters for search engines.
loading.tsx and error.tsx in app/ and override them only where needed.notFound() or redirect() in try/catch: both work by throwing, and a catch would swallow them.error.digest to your monitoring service from a useEffect in error.tsx.Why must `error.tsx` start with `"use client"`?
loading.tsx wraps the segment in Suspense and streams a fallback while the page loads.error.tsx is a client-side Error Boundary with error and reset props; global-error.tsx covers the root.error.tsx does not catch its own layout's errors; move the boundary up a level.not-found.tsx renders for unmatched URLs and whenever notFound() is called.notFound() and redirect() throw, so keep them outside try/catch blocks.Next lesson: Route Groups and Project Organization ā group routes without affecting URLs and keep a large app/ folder tidy.