Skip to content
C.W.K.
Stream
Lesson 09 of 10 · published

Loading, Error & Not Found

~22 min · loading, error, not-found, Suspense

Level 0Curious
0 XP0/68 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

Three special files, three behaviors

FileWhat it doesServer or Client?
loading.tsxWraps the page in Suspense; shown while data loadsEither
error.tsxError boundary for the segment and belowMust be Client
not-found.tsxUI for notFound() calls in this segmentEither

global-error.tsx for the root

An error.tsx can't catch errors in its own layout. The root layout has a special boundary: app/global-error.tsx. It must render its own <html> and <body> because it replaces the broken root layout.

Trigger 404 from anywhere

Call notFound() from next/navigation in a Server Component to bail out and render the nearest not-found.tsx. The framework handles the status code (404) for you.

Classify a failure before choosing its file. Waiting belongs to loading.tsx, an expected missing resource belongs to notFound(), and an unexpected exception belongs to error.tsx. That keeps user action, HTTP status, and operational reporting aligned. Inject a delay and an exception at each boundary, then record which layouts remain mounted. After recovery, verify that retry preserves the same input and navigation context. Turning every failure into a friendly 200 response hides incidents from crawlers and monitoring. Throwing validation failures into an error boundary is the opposite mistake: it destroys useful form context. Group failures by recovery behavior, not by the fact that they are inconvenient. Create one slow Promise, one missing ID, one child render exception, and one root-layout exception. Verify which boundary handles each case and which HTTP status leaves the server. Confirm global-error.tsx can render without the normal root shell.

Code

Loading skeleton wired automatically·tsx
// app/dashboard/loading.tsx
export default function Loading() {
  return (
    <div className="animate-pulse space-y-3 p-6">
      <div className="h-6 w-1/3 rounded bg-gray-200" />
      <div className="h-4 w-full rounded bg-gray-200" />
      <div className="h-4 w-5/6 rounded bg-gray-200" />
    </div>
  );
}

// app/dashboard/page.tsx — slow data triggers the boundary
export default async function Dashboard() {
  const data = await fetchSlow();
  return <DashboardContent data={data} />;
}
Per-segment error boundary·tsx
// app/dashboard/error.tsx
'use client';
import { useEffect } from 'react';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // ship to Sentry / your logger
    console.error(error);
  }, [error]);

  return (
    <div className="p-6 text-center">
      <h2 className="text-lg font-semibold">Something went wrong</h2>
      <p className="text-sm text-gray-500 mt-1">{error.message}</p>
      <button
        onClick={() => reset()}
        className="mt-4 rounded bg-blue-600 px-4 py-2 text-white"
      >
        Try again
      </button>
    </div>
  );
}
404 from inside a Server Component·tsx
import { notFound } from 'next/navigation';

export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const post = await getPost(id);
  if (!post) notFound();
  return <Article post={post} />;
}

External links

Exercise

Add loading.tsx, error.tsx, and not-found.tsx to a route. Throw an error inside the page and confirm the error boundary catches it; call notFound() for an unknown id and confirm the not-found UI shows with a 404 status in the network tab.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.