LearnFrenzy
Sign in Take a free test

Layouts and Routing · Lesson 7 of 24

Loading, Error and Not Found Pages in Next.js

6 min read Updated Nov 8, 2026 Saurabh Samir

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 state
  • error.tsx: catching errors with a retry button
  • notFound() and not-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>;
}

Visiting /leaderboard

› 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>
  );
}

Visiting /stats

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.tsx catches errors in its segment's pages and nested segments, but not in the layout.tsx of the same folder – put a boundary one level up for that.
  • app/global-error.tsx catches 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 catchError from next/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>
  );
}

HTTP response and visit

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

KindExamplesHandle with
Expectedvalidation failed, item not found, payment declinedreturn a value and render a message (notFound(), useActionState results)
Uncaughtbugs, a service is down, unexpected nullsthrow, and let error.tsx show a fallback

Practice

Try it yourself

  1. Add a site-wide app/not-found.tsx with 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>;
    }

    Visits

    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.

Next lesson

Search LearnFrenzy

Esc

Quick links