Skip to main content

backend-error-handling

Implement error-handling patterns (boundaries, toasts, API error shape). Use when "error boundary", "error toast", or "standardize API errors". Plan-only observability audit → plan-error-handling. Live Sentry triage → debug-sentry-monitor.

Source facts

Repository
kensaurus/cursor-kenji
Last source activity
September 12, 2026 at 14:20
Detected SKILL.md language
English
Stars
9
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
2 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
backend-error-handling
description
Implement error-handling patterns (boundaries, toasts, API error shape). Use when "error boundary", "error toast", or "standardize API errors". Plan-only observability audit → plan-error-handling. Live Sentry triage → debug-sentry-monitor.
license
MIT
# Error Handling Skill **Degree of freedom: MIXED.** Which layer and message `[HIGH freedom]`; existing-type/boundary probes `[LOW freedom — run exactly]`. ## How to reason 1. **Observe** — existing error types, boundaries, `ActionResult` / `ApiError` 2. **Interpret** — missing layer vs inconsistent shape vs swallowed catch 3. **Classify** — reuse-existing / extend-codes / add-boundary / toast-only 4. **Severity** — unhandled 500 on a mutation outranks a missing toast ## Worked example > **Observe:** `createUser` throws Prisma `P2002`; UI shows a blank catch; `ActionResult` already lives in `types/errors.ts`. > **Interpret:** known conflict is unmapped; no field/toast path. > **Classify:** reuse `ActionResult` + `CONFLICT`; toast the message; do not add a second error type. > **Verify:** duplicate email returns `{ success: false, error: { code: 'CONFLICT' } }`; form alert shown. ## Self-critique before reporting - **Reuse shape** — searched `ActionResult` / `ApiError` / `error.tsx` before adding a type - **User-safe** — `INTERNAL_ERROR` is generic; PII is not in the client message - **Layered** — boundary + action result + toast, not toast-only - **Right owner** — plan-only observability audit → `plan-error-handling`; live Sentry triage → `debug-sentry-monitor` full error handling patterns for full-stack applications. ## When to Use - Adding error handling to new features - Improving error user experience - Standardizing error responses - Debugging error propagation - Adding error monitoring ## Check existing first [LOW freedom — run exactly] **Before adding ANY error handling, verify:** 1. **Check for existing error types:** ```bash rg "type.*Error|interface.*Error" --type ts rg "ActionResult|ApiError" --type ts ``` 2. **Check for existing error boundaries:** ```bash ls -la app/error.tsx app/global-error.tsx rg "ErrorBoundary" --type tsx ``` 3. **Check for existing error utilities:** ```bash rg "formatError|handleError|reportError" --type ts ls -la src/lib/errors* src/lib/error* 2>/dev/null # @/lib/errors ``` 4. **Check established error response patterns:** ```bash rg "success: false|error:" src/features/*/server/ --type ts | head -10 ``` **Why:** Inconsistent error handling confuses users and complicates debugging. Always follow established patterns. ## Error Handling Layers [HIGH freedom] ``` ┌─────────────────────────────────────────┐ │ UI Layer │ │ - Error boundaries │ │ - Form validation errors │ │ - Toast notifications │ ├─────────────────────────────────────────┤ │ Application Layer │ │ - Server Action errors │ │ - API route errors │ │ - Business logic errors │ ├─────────────────────────────────────────┤ │ Data Layer │ │ - Database errors │ │ - Validation errors (Zod) │ │ - External API errors │ └─────────────────────────────────────────┘ ``` ## Standard Error Types [HIGH freedom] ```typescript // types/errors.ts // Base error shape interface AppError { code: string // Machine-readable: VALIDATION_ERROR message: string // User-friendly message details?: unknown // Additional context } // Action result pattern type ActionResult<T> = | { success: true; data: T } | { success: false; error: AppError } // Common error codes const ErrorCode = { VALIDATION_ERROR: 'VALIDATION_ERROR', NOT_FOUND: 'NOT_FOUND', UNAUTHORIZED: 'UNAUTHORIZED', FORBIDDEN: 'FORBIDDEN', CONFLICT: 'CONFLICT', RATE_LIMITED: 'RATE_LIMITED', INTERNAL_ERROR: 'INTERNAL_ERROR', } as const ``` ## Server Action Error Handling [HIGH freedom] ```typescript // features/users/server/actions.ts 'use server' import { z } from 'zod' import { revalidatePath } from 'next/cache' const CreateUserSchema = z.object({ email: z.string().email('Invalid email address'), name: z.string().min(1, 'Name is required'), }) export async function createUser( prevState: ActionResult<User>, formData: FormData ): Promise<ActionResult<User>> { try { // 1. Validate input const validated = CreateUserSchema.safeParse({ email: formData.get('email'), name: formData.get('name'), }) if (!validated.success) { return { success: false, error: { code: 'VALIDATION_ERROR', message: 'Invalid input', details: validated.error.flatten().fieldErrors, }, } } // 2. Check authorization const session = await auth() if (!session) { return { success: false, error: { code: 'UNAUTHORIZED', message: 'Please sign in to continue', }, } } // 3. Execute business logic const user = await db.user.create({ data: validated.data, }) revalidatePath('/users') return { success: true, data: user } } catch (error) { // 4. Handle known errors if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error.code === 'P2002') { return { success: false, error: { code: 'CONFLICT', message: 'A user with this email already exists', }, } } } // 5. Log unknown errors, return generic message console.error('createUser error:', error) return { success: false, error: { code: 'INTERNAL_ERROR', message: 'Something went wrong. Please try again.', }, } } } ``` ## Form Error Display (React 19+) [HIGH freedom] ```tsx // components/UserForm.tsx 'use client' import { useActionState } from 'react' import { useFormStatus } from 'react-dom' import { createUser } from '@/features/users/server/actions' // Separate submit button to use useFormStatus function SubmitButton() { const { pending } = useFormStatus() return ( <button type="submit" disabled={pending}> {pending ? 'Creating...' : 'Create User'} </button> ) } export function UserForm() { const [state, action, isPending] = useActionState(createUser, null) // Get field errors from validation const fieldErrors = state?.success === false ? state.error.details as Record<string, string[]> : {} return ( <form action={action}> {/* Global error */} {state?.success === false && state.error.code !== 'VALIDATION_ERROR' && ( <div role="alert" className="bg-red-50 text-red-700 p-3 rounded-lg mb-4"> {state.error.message} </div> )} {/* Field with error */} <div> <label htmlFor="email">Email</label> <input id="email" name="email" type="email" aria-invalid={!!fieldErrors.email} aria-describedby={fieldErrors.email ? 'email-error' : undefined} className={fieldErrors.email ? 'border-red-500' : ''} /> {fieldErrors.email && ( <p id="email-error" className="text-red-600 text-sm mt-1"> {fieldErrors.email[0]} </p> )} </div> <SubmitButton /> </form> ) } ``` **React 19 Form Patterns:** - `useActionState` - Form state with Server Actions - `useFormStatus` - Pending state in child components - `useOptimistic` - Optimistic UI updates ``` ## React Error Boundaries [HIGH freedom] ```tsx // app/error.tsx (Next.js page error boundary) 'use client' import { useEffect } from 'react' export default function Error({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { useEffect(() => { // Log to error reporting service console.error('Page error:', error) }, [error]) return ( <div className="flex flex-col items-center justify-center min-h-[400px]"> <h2 className="text-xl font-semibold mb-4">Something went wrong</h2> <p className="text-muted-foreground mb-6"> We're sorry, but something unexpected happened. </p> <button onClick={reset} className="px-4 py-2 bg-primary text-primary-foreground rounded-lg" > Try again </button> </div> ) } // app/global-error.tsx (root error boundary) '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> ) } ``` ## API Route Error Handling [HIGH freedom] ```typescript // app/api/products/route.ts import { NextRequest, NextResponse } from 'next/server' import { z } from 'zod' export async function POST(request: NextRequest) { try { const body = await request.json() const validated = ProductSchema.safeParse(body) if (!validated.success) { return NextResponse.json( { error: { code: 'VALIDATION_ERROR', message: 'Invalid request body', details: validated.error.flatten().fieldErrors, }, }, { status: 400 } ) } const product = await db.product.create({ data: validated.data }) return NextResponse.json({ data: product }, { status: 201 }) } catch (error) { if (error instanceof SyntaxError) { return NextResponse.json( { error: { code: 'INVALID_JSON', message: 'Invalid JSON body' } }, { status: 400 } ) } console.error('POST /api/products error:', error) return NextResponse.json( { error: { code: 'INTERNAL_ERROR', message: 'Internal server error' } }, { status: 500 } ) } } ``` ## TanStack Query Error Handling [HIGH freedom] ```tsx // hooks/useProducts.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' export function useProducts() { return useQuery({ queryKey: ['products'], queryFn: async () => { const res = await fetch('/api/products') if (!res.ok) { const error = await res.json() throw new Error(error.error?.message || 'Failed to fetch products') } return res.json() }, retry: (failureCount, error) => { // Don't retry on 4xx errors if (error.message.includes('401') || error.message.includes('403')) { return false } return failureCount < 3 }, }) } export function useCreateProduct() { const queryClient = useQueryClient() return useMutation({ mutationFn: async (data: ProductInput) => { const res = await fetch('/api/products', { method: 'POST', body: JSON.stringify(data), }) if (!res.ok) { const error = await res.json() throw new Error(error.error?.message || 'Failed to create product') } return res.json() }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['products'] }) toast.success('Product created') }, onError: (error) => { toast.error(error.message) }, }) } ``` ## Error State UI Components [HIGH freedom] ```tsx // components/ErrorState.tsx import { AlertCircle, RefreshCw } from 'lucide-react' interface ErrorStateProps { title?: string message: string onRetry?: () => void } export function ErrorState({ title = 'Error', message, onRetry }: ErrorStateProps) { return ( <div className="flex flex-col items-center justify-center py-12 text-center"> <AlertCircle className="h-12 w-12 text-red-500 mb-4" /> <h3 className="font-semibold text-lg mb-2">{title}</h3>
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub