Layouts and Routing · Lesson 7 of 24
Loading, Error and Not Found Pages in Next.js
Real pages wait for data, fail sometimes, and get visited with URLs that don't exist. Next.js has a special file for each case – loading.tsx, error.tsx and not-found.tsx – that you drop into a route folder. Behind the scenes they're React Suspense and error boundaries placed around your page automatically.
In this lesson
loading.tsx: an instant loading stateerror.tsx: catching errors with a retry buttonnotFound()andnot-found.tsx- Expected errors vs uncaught exceptions
loading.tsx
This page waits 1.5 seconds for its data on every request. connection() tells Next.js the page needs a real request (so it isn't prerendered at build time):
// app/leaderboard/page.tsx
import { connection } from "next/server";
async function getLeaders() {
await connection(); // request-time data
await new Promise((r) => setTimeout(r, 1500)); // a slow database query
return ["Asha – 1,240 pts", "Ravi – 1,180 pts", "Meera – 990 pts"];
}
export default async function Leaderboard() {
const leaders = await getLeaders();
return (
<main>
<h1>Leaderboard</h1>
<ol>{leaders.map((l) => <li key={l}>{l}</li>)}</ol>
</main>
);
}
// app/leaderboard/loading.tsx
export default function Loading() {
return <p>Loading leaderboard…</p>;
}
› 700 ms after opening the page Loading leaderboard… › fully loaded Leaderboard 1. Asha – 1,240 pts 2. Ravi – 1,180 pts 3. Meera – 990 pts
The loading UI is shown immediately – it's part of the prerendered shell – and the real content streams in over the same response when it's ready. Layouts above loading.tsx stay visible and interactive the whole time. A skeleton that looks like the final page usually feels faster than a spinner.
error.tsx
An error.tsx catches errors thrown while rendering its route segment and shows a fallback instead of crashing the whole site. It must be a Client Component, and it receives the error and a retry function:
// app/stats/page.tsx
import { Suspense } from "react";
import { connection } from "next/server";
let requests = 0;
async function LearnerCount() {
await connection();
requests++;
if (requests === 1) throw new Error("Analytics service unavailable"); // fails on the first request only
return <p>12,840 learners this month</p>;
}
export default function Stats() {
return (
<main>
<h1>Stats</h1>
<Suspense fallback={<p>Loading…</p>}>
<LearnerCount />
</Suspense>
</main>
);
}
// app/stats/error.tsx
"use client";
export default function StatsError({ error, retry }: { error: unknown; retry: () => void }) {
return (
<div role="alert">
<h2>Couldn't load the stats</h2>
<button onClick={() => retry()}>Try again</button>
</div>
);
}
Couldn't load the stats [Try again] › click “Try again” Stats 12,840 learners this month
retry() re-fetches and re-renders the segment; the second attempt succeeds. In production, error messages from Server Components are hidden from the browser (only a digest id is sent) so you don't leak secrets – log the real error on the server and show a friendly message.
Where errors go
error.tsxcatches errors in its segment's pages and nested segments, but not in thelayout.tsxof the same folder – put a boundary one level up for that.app/global-error.tsxcatches errors in the root layout; it replaces the whole page, so it renders its own<html>and<body>.- For a boundary around just one component, use
catchErrorfromnext/error.
notFound() and not-found.tsx
// app/courses/[slug]/page.tsx
import { notFound } from "next/navigation";
const courses: Record<string, string> = { react: "React.js", nextjs: "Next.js" };
export function generateStaticParams() {
return Object.keys(courses).map((slug) => ({ slug }));
}
export default async function Course({ params }: PageProps<"/courses/[slug]">) {
const { slug } = await params;
const title = courses[slug];
if (!title) notFound();
return <h1>{title}</h1>;
}
// app/courses/[slug]/not-found.tsx
import Link from "next/link";
export default function CourseNotFound() {
return (
<main>
<h1>Course not found</h1>
<p>We don't have that course yet.</p>
<Link href="/courses/react">Try the React course</Link>
</main>
);
}
GET /courses/angular → 200 text/html (HTML page with <meta name="robots" content="noindex">) URL: /courses/angular Course not found We don't have that course yet. Try the React course › click “Try the React course” URL: /courses/react React.js
Look at the HTTP response: the status is 200, not 404. Because angular isn't in generateStaticParams, Next.js sent the route's prerendered shell first and checked the slug while streaming – once streaming has started, the status can't change any more. Next.js therefore adds <meta name="robots" content="noindex"> so search engines still drop the page. When notFound() runs before streaming starts – for an unmatched URL, or a check in proxy.ts – the response is a real 404, as the practice task below shows.
An app/not-found.tsx at the root handles every unmatched URL on the site.
Expected errors vs uncaught exceptions
| Kind | Examples | Handle with |
|---|---|---|
| Expected | validation failed, item not found, payment declined | return a value and render a message (notFound(), useActionState results) |
| Uncaught | bugs, a service is down, unexpected nulls | throw, and let error.tsx show a fallback |
Practice
Try it yourself
Add a site-wide
app/not-found.tsxwith a link home, and visit a URL that doesn't exist.Show solution
// app/not-found.tsx import Link from "next/link"; export default function NotFound() { return ( <main> <h1>404 – Page not found</h1> <Link href="/">Go to the home page</Link> </main> ); }// app/page.tsx export default function Home() { return <h1>Home</h1>; }GET /no-such-page → 404 text/html (HTML page with <meta name="robots" content="noindex">) URL: /no-such-page 404 – Page not found Go to the home page › click “Go to the home page” URL: / Home
Quick check
Why must error.tsx be a Client Component?
Error boundaries are interactive (retry) and run in the browser.
What does retry() do?
Re-fetches and re-renders the failed segment.
What status code does notFound() produce?
404.
Summary
- loading.tsx shows instant fallback UI while the page streams in.
- error.tsx (a Client Component) catches render errors and offers retry(); global-error.tsx covers the root layout.
- notFound() renders not-found.tsx with a real 404 status.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.