SyntaxStudy
Sign Up
Next.js Loading UI, Error Boundaries, and Not Found
Next.js Beginner 1 min read

Loading UI, Error Boundaries, and Not Found

Next.js provides special files that integrate with React Suspense and error boundaries at the route level. A loading.tsx file automatically wraps the page.tsx in a Suspense boundary. While the server component fetches data, Next.js immediately streams the loading UI to the client. This means users see a skeleton or spinner instantly rather than a blank screen, dramatically improving perceived performance. Error boundaries catch runtime errors thrown during rendering, in lifecycle methods, and in constructors of child components. The error.tsx file must be a client component ('use client') because it receives the error object and a reset function as props. Calling reset() re-renders the segment, allowing the user to recover without a full page reload. You can nest error.tsx files to scope error handling to specific route segments. The not-found.tsx file renders when the notFound() function from next/navigation is called inside a server component, or when a URL does not match any route. Calling notFound() immediately stops rendering and renders the closest not-found.tsx up the segment tree. A root app/not-found.tsx acts as the global 404 page for your entire application.
Example
// app/dashboard/loading.tsx
// Shown automatically while dashboard/page.tsx loads
export default function DashboardLoading() {
  return (
    <div className="space-y-4 animate-pulse">
      <div className="h-8 bg-gray-200 rounded w-1/3" />
      <div className="h-32 bg-gray-200 rounded" />
      <div className="h-32 bg-gray-200 rounded" />
    </div>
  );
}

// app/dashboard/error.tsx
'use client';
import { useEffect } from 'react';

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

  return (
    <div className="text-center py-16">
      <h2 className="text-xl font-semibold">Something went wrong!</h2>
      <button
        onClick={reset}
        className="mt-4 px-4 py-2 bg-blue-600 text-white rounded"
      >
        Try again
      </button>
    </div>
  );
}

// app/blog/[slug]/page.tsx — trigger 404
import { notFound } from 'next/navigation';

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await fetchPost(params.slug);
  if (!post) notFound(); // renders app/not-found.tsx
  return <article>{post.content}</article>;
}