Next.js

Lesson 07

Loading and error boundaries

Give dynamic routes immediate loading UI and route-level recovery.

Good Code

app/dashboard/reviews/error.tsx
"use client";

import { useEffect } from "react";

export default function ReviewsError({
  error,
  unstable_retry,
}: {
  error: Error & { digest?: string };
  unstable_retry: () => void;
}) {
  useEffect(() => {
    console.error(error);
  }, [error]);

  // A route error boundary can log, show fallback UI, and trigger retry.
  return (
    <section role="alert">
      <h2>Reviews could not load.</h2>
      <button type="button" onClick={() => unstable_retry()}>
        Try again
      </button>
    </section>
  );
}

Bad Code

app/dashboard/reviews/page.tsx
import { getReviews } from "@/lib/reviews";

export default async function ReviewsPage() {
  // Catching every error in the page hides failures from the route boundary.
  try {
    const reviews = await getReviews();
    return reviews.map((review) => <p key={review.id}>{review.title}</p>);
  } catch {
    return <p>Something went wrong.</p>;
  }
}

Review Notes

What to review

Good Code

The good version uses a route-level error.tsx Client Component so unexpected render failures are contained and retryable.

Bad Code

The bad version catches every exception inside the page, hiding failures from the route boundary and leaving navigation without a meaningful loading strategy.

Takeaways

  • Use loading and error files to keep navigation responsive and failures contained.