Use when you have a written implementation plan to execute in a separate session with review checkpoints
npx skills add soilmass/vibe-coding-plugin --skill "error-handling"
Install specific skill from multi-skill repository
# Description
>
# SKILL.md
name: error-handling
description: >
Next.js 15 App Router error boundaries — error.tsx, global-error.tsx, not-found.tsx, reset recovery, redirect/notFound throw semantics
allowed-tools: Read, Grep, Glob
Error Handling
Purpose
Error boundary patterns for Next.js 15 App Router. Covers error.tsx, global-error.tsx,
not-found.tsx, and recovery via reset(). The ONE skill for runtime error UI.
When to Use
- Adding error boundaries to route segments
- Implementing 404 / not-found pages
- Handling runtime errors with recovery UI
- Debugging why
redirect()ornotFound()throws unexpectedly
When NOT to Use
- Form validation errors →
react-forms - Server Action error responses →
react-server-actions - API route error responses →
api-routes
Pattern
error.tsx (must be "use client")
"use client";
export default function Error({
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>
);
}
global-error.tsx (must include html/body)
"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>
);
}
not-found.tsx
import Link from "next/link";
export default function NotFound() {
return (
<div>
<h2>Not Found</h2>
<Link href="/">Go home</Link>
</div>
);
}
Suspense error recovery pattern
"use client";
import { useTransition } from "react";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const [isPending, startTransition] = useTransition();
return (
<div role="alert">
<h2>Something went wrong</h2>
<p>{error.digest ? `Error ID: ${error.digest}` : error.message}</p>
<button
onClick={() => startTransition(() => reset())}
disabled={isPending}
>
{isPending ? "Retrying..." : "Try again"}
</button>
</div>
);
}
Retry with exponential backoff helper
// src/lib/retry.ts
export async function withRetry<T>(
fn: () => Promise<T>,
options: { maxAttempts?: number; baseDelay?: number } = {},
): Promise<T> {
const { maxAttempts = 3, baseDelay = 1000 } = options;
let lastError: unknown;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt < maxAttempts - 1) {
const delay = baseDelay * Math.pow(2, attempt); // 1s, 2s, 4s
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
// Usage:
const data = await withRetry(() => fetch("https://api.example.com/data"));
Async Server Component error handling
When an async Server Component throws, the nearest error.tsx boundary catches it.
error.tsx must be "use client". Use notFound() to trigger not-found.tsx instead.
// Async Server Component — throw triggers nearest error.tsx
export default async function PostPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const post = await db.post.findUnique({ where: { id } });
if (!post) notFound(); // Triggers not-found.tsx (NOT error.tsx)
return <article>{post.title}</article>;
// If db.post.findUnique throws → nearest error.tsx catches it
}
Anti-pattern
// WRONG: wrapping redirect() in try-catch
async function loadPage(id: string) {
try {
const data = await getData(id);
if (!data) redirect("/404"); // throws NEXT_REDIRECT — NOT a real error
} catch (e) {
// This catches the redirect throw, breaking navigation!
console.error(e);
}
}
// WRONG: catching redirect/revalidate in Server Actions
export async function updatePost(formData: FormData) {
try {
await db.post.update({ ... });
revalidateTag("posts"); // throws NEXT_REVALIDATE internally
redirect("/posts"); // throws NEXT_REDIRECT
} catch (e) {
// Catches BOTH the revalidate and redirect throws!
// Move redirect() outside try-catch or re-throw non-errors
}
}
// CORRECT: catch only specific errors, let framework throws propagate
export async function updatePost(formData: FormData) {
try {
await db.post.update({ ... });
} catch (e) {
return { error: "Update failed" };
}
revalidateTag("posts"); // Outside try-catch
redirect("/posts"); // Outside try-catch
}
redirect() and notFound() work by throwing special errors. Wrapping them in
try-catch intercepts the throw and prevents Next.js from handling it.
notFound() throws NEXT_NOT_FOUND — same semantics.
Common Mistakes
- Forgetting
"use client"onerror.tsx— it MUST be a Client Component - Missing
global-error.tsx— root layout errors have no boundary without it - Wrapping
redirect()in try-catch — it throwsNEXT_REDIRECT, not a real error - Wrapping
notFound()in try-catch — same issue, throwsNEXT_NOT_FOUND - Not providing
reset()button — users get stuck on error screens - Catching
revalidateTag/redirectin Server Action try-catch blocks - Broad error catching — catch specific errors, not all errors
Checklist
- [ ]
error.tsxhas"use client"directive - [ ]
global-error.tsxincludes<html>and<body>tags - [ ]
not-found.tsxexists for custom 404 pages - [ ]
redirect()andnotFound()are NOT inside try-catch - [ ] Error UI provides
reset()recovery action - [ ] External API calls use retry with exponential backoff
- [ ] Error messages show
digestID (not stack traces) in production
Composes With
nextjs-routing— error files are route segment conventionsreact-suspense— Suspense boundaries handle loading, error boundaries handle failuressecurity— error messages should not leak sensitive detailslogging— errors should be logged for debugging and monitoring
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.