963 markdown files reformatted with the repository's pinned prettier so pnpm format:check covers the folded tree like every other repo file. The formatter's embedded-language pass also normalized code fences (TS semicolons, closed HTML tags in examples, lowercased CSS hex colors, one renumbered list that skipped an index). Alphanumeric token deltas vs the fold commit were audited file-by-file; all are formatter-equivalent markup normalizations plus the four sanitized skills.
4.7 KiB
4.7 KiB
Error Handling
Handle errors gracefully in Next.js applications.
Reference: https://nextjs.org/docs/app/getting-started/error-handling
Error Boundaries
error.tsx
Catches errors in a route segment and its children:
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
Important: error.tsx must be a Client Component.
global-error.tsx
Catches errors in root layout:
'use client';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
);
}
Important: Must include <html> and <body> tags.
Server Actions: Navigation API Gotcha
Do NOT wrap navigation APIs in try-catch. They throw special errors that Next.js handles internally.
Reference: https://nextjs.org/docs/app/api-reference/functions/redirect#behavior
'use server'
import { redirect } from 'next/navigation'
import { notFound } from 'next/navigation'
// Bad: try-catch catches the navigation "error"
async function createPost(formData: FormData) {
try {
const post = await db.post.create({ ... })
redirect(`/posts/${post.id}`) // This throws!
} catch (error) {
// redirect() throw is caught here - navigation fails!
return { error: 'Failed to create post' }
}
}
// Good: Call navigation APIs outside try-catch
async function createPost(formData: FormData) {
let post
try {
post = await db.post.create({ ... })
} catch (error) {
return { error: 'Failed to create post' }
}
redirect(`/posts/${post.id}`) // Outside try-catch
}
// Good: Re-throw navigation errors
async function createPost(formData: FormData) {
try {
const post = await db.post.create({ ... })
redirect(`/posts/${post.id}`)
} catch (error) {
if (error instanceof Error && error.message === 'NEXT_REDIRECT') {
throw error // Re-throw navigation errors
}
return { error: 'Failed to create post' }
}
}
Same applies to:
redirect()- 307 temporary redirectpermanentRedirect()- 308 permanent redirectnotFound()- 404 not foundforbidden()- 403 forbiddenunauthorized()- 401 unauthorized
Use unstable_rethrow() to re-throw these errors in catch blocks:
import { unstable_rethrow } from 'next/navigation';
async function action() {
try {
// ...
redirect('/success');
} catch (error) {
unstable_rethrow(error); // Re-throws Next.js internal errors
return { error: 'Something went wrong' };
}
}
Redirects
import { redirect, permanentRedirect } from 'next/navigation';
// 307 Temporary - use for most cases
redirect('/new-path');
// 308 Permanent - use for URL migrations (cached by browsers)
permanentRedirect('/new-url');
Auth Errors
Trigger auth-related error pages:
import { forbidden, unauthorized } from 'next/navigation';
async function Page() {
const session = await getSession();
if (!session) {
unauthorized(); // Renders unauthorized.tsx (401)
}
if (!session.hasAccess) {
forbidden(); // Renders forbidden.tsx (403)
}
return <Dashboard />;
}
Create corresponding error pages:
// app/forbidden.tsx
export default function Forbidden() {
return <div>You don't have access to this resource</div>;
}
// app/unauthorized.tsx
export default function Unauthorized() {
return <div>Please log in to continue</div>;
}
Not Found
not-found.tsx
Custom 404 page for a route segment:
export default function NotFound() {
return (
<div>
<h2>Not Found</h2>
<p>Could not find the requested resource</p>
</div>
);
}
Triggering Not Found
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(); // Renders closest not-found.tsx
}
return <div>{post.title}</div>;
}
Error Hierarchy
Errors bubble up to the nearest error boundary:
app/
├── error.tsx # Catches errors from all children
├── blog/
│ ├── error.tsx # Catches errors in /blog/*
│ └── [slug]/
│ ├── error.tsx # Catches errors in /blog/[slug]
│ └── page.tsx
└── layout.tsx # Errors here go to global-error.tsx