Topic 12 of 47
Loading & Error UI
Overview
Next.js has special file conventions for loading states and error UIs — loading.tsx and error.tsx. These automatically wrap page content with Suspense and ErrorBoundary without any configuration, making skeleton screens and error recovery simple.
Syntax
tsx
// app/dashboard/loading.tsx — automatic Suspense fallback
export default function DashboardLoading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-4" />
<div className="grid grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="h-32 bg-gray-200 rounded" />
))}
</div>
</div>
);
}
// app/dashboard/error.tsx — automatic ErrorBoundary
'use client'; // must be client component
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}Common Pitfalls
- loading.tsx wraps the entire page — for more granular loading, use Suspense directly around individual components.
- error.tsx MUST be a Client Component ('use client') — it uses React's ErrorBoundary under the hood which is class-based.
- Interview tip: The reset() function in error.tsx re-renders the component tree — it gives users a way to recover without a full page reload.
Real-World Example
Skeleton loading state for a dashboard page
example
tsx
// app/orders/loading.tsx
export default function OrdersLoading() {
return (
<div className="space-y-4 p-6">
<div className="flex justify-between items-center">
<div className="h-8 bg-gray-200 rounded w-40 animate-pulse" />
<div className="h-10 bg-gray-200 rounded w-32 animate-pulse" />
</div>
<div className="rounded-lg border overflow-hidden">
{/* Table skeleton */}
<div className="bg-gray-50 p-4 flex gap-4">
{["Order ID", "Customer", "Total", "Status"].map(col => (
<div key={col} className="h-4 bg-gray-200 rounded flex-1 animate-pulse" />
))}
</div>
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="p-4 border-t flex gap-4">
{Array.from({ length: 4 }).map((_, j) => (
<div key={j} className="h-4 bg-gray-200 rounded flex-1 animate-pulse" />
))}
</div>
))}
</div>
</div>
);
}