patterntypescriptnextjsModerate
loading.tsx and error.tsx conventions in App Router
Viewed 0 times
Next.js 13+ with App Router
loading.tsxerror.tsxsuspenseerror boundaryroute-level loadingglobal-error
Error Messages
Problem
Showing loading states and handling errors for server-rendered pages requires manual Suspense boundaries and error boundaries. Forgetting them leads to blank screens during data fetching.
Solution
Place loading.tsx and error.tsx files next to page.tsx — Next.js auto-wraps them:
// app/dashboard/loading.tsx — auto-shown while page.tsx suspends
export default function DashboardLoading() {
return (
<div className='animate-pulse'>
<div className='h-8 bg-gray-200 rounded w-1/4 mb-4' />
<div className='h-32 bg-gray-200 rounded' />
</div>
);
}
// app/dashboard/error.tsx — wraps page.tsx in an error boundary
'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>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
// app/dashboard/page.tsx — can throw or be async
export default async function DashboardPage() {
const data = await fetchDashboard();
return <Dashboard data={data} />;
}
// app/dashboard/loading.tsx — auto-shown while page.tsx suspends
export default function DashboardLoading() {
return (
<div className='animate-pulse'>
<div className='h-8 bg-gray-200 rounded w-1/4 mb-4' />
<div className='h-32 bg-gray-200 rounded' />
</div>
);
}
// app/dashboard/error.tsx — wraps page.tsx in an error boundary
'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>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
// app/dashboard/page.tsx — can throw or be async
export default async function DashboardPage() {
const data = await fetchDashboard();
return <Dashboard data={data} />;
}
Why
Next.js wraps page.tsx in a Suspense boundary using loading.tsx as the fallback, and in a React error boundary using error.tsx. This eliminates boilerplate and ensures loading/error states are handled at the route level.
Gotchas
- error.tsx MUST be a Client Component ('use client') because error boundaries require client hooks
- The reset() function re-renders the error boundary — it re-fetches the page component
- global-error.tsx in the app root catches errors in the root layout — it must include <html> and <body>
- loading.tsx wraps the entire page — for granular loading, use Suspense manually inside the page
Code Snippets
error.tsx with reset functionality
'use client';
export default function Error({ error, reset }: {
error: Error; reset: () => void;
}) {
return (
<div>
<p>Error: {error.message}</p>
<button onClick={reset}>Retry</button>
</div>
);
}Context
When adding loading states and error handling to Next.js App Router pages
Revisions (0)
No revisions yet.