# Error Handling Structured logging, typed custom errors, a React Error Boundary, and API route wrappers for Next.js 14 — gives every thrown error a consistent shape and HTTP status code. ## What's included **Logger** - `logger` — structured JSON logger with `debug`, `info`, `warn`, `error` methods; each entry includes `level`, `message`, `context`, `timestamp`, and a short `requestId`; production `error` calls include a commented hook to forward to Axiom/Datadog **Custom error classes** (all extend `AppError`) - `AppError` — base class; takes `message`, `code`, `statusCode`, optional `context`; has `toJSON()` for response serialization - `NotFoundError` — 404, code `NOT_FOUND`; accepts `resource` and optional `id` - `UnauthorizedError` — 401, code `UNAUTHORIZED` - `ForbiddenError` — 403, code `FORBIDDEN` - `ValidationError` — 422, code `VALIDATION_ERROR`; accepts a `fields` map for per-field errors - `RateLimitError` — 429, code `RATE_LIMITED`; stores `retryAfter` in context - `ExternalServiceError` — 502, code `EXTERNAL_SERVICE_ERROR`; prefixes message with service name **API route helpers** - `handleApiError(err)` — catches `AppError` subclasses and unknown errors; returns a typed `Response.json` with the right status code - `withErrorHandler(handler)` — wraps an async route handler in try/catch; calls `handleApiError` on throw; drop-in for `GET`, `POST`, etc. **Async utility** - `tryCatch(fn, context?)` — Go-style `[data, null] | [null, Error]` tuple; logs the error with optional context label before returning **React** - `ErrorBoundary` — class component; logs via `logger.error` in `componentDidCatch`; accepts a custom `fallback` component receiving `{ error, errorId, reset }`; falls back to `DefaultErrorFallback` if none provided - `DefaultErrorFallback` — renders error message + error ID + "Try Again" / "Go Home" buttons ## Setup ### 1. Install dependencies No extra packages — React and Next.js only. ### 2. Add to your project ```ts // Any API route import { withErrorHandler, NotFoundError } from '@/blocks/errorhandling' // Any client component tree import { ErrorBoundary } from '@/blocks/errorhandling' ``` ### 3. Wire up production logging (optional) In `logger._log`, uncomment and replace the `fetch('/api/log', ...)` line with your logging service's ingest endpoint. ## Usage examples ```ts // API route — withErrorHandler wraps the whole handler import { withErrorHandler, NotFoundError, ValidationError } from '@/blocks/errorhandling' export const GET = withErrorHandler(async (req) => { const block = await getBlock(id) if (!block) throw new NotFoundError('Block', id) return Response.json(block) }) export const POST = withErrorHandler(async (req) => { const body = await req.json() if (!body.email) throw new ValidationError('Invalid input', { email: 'Required' }) return Response.json({ ok: true }) }) ``` ```ts // tryCatch for async calls where you want to handle errors inline import { tryCatch, handleApiError } from '@/blocks/errorhandling' const [user, err] = await tryCatch(() => fetchUser(id), 'fetchUser') if (err) return handleApiError(err) ``` ```tsx // Wrap any subtree that might throw during render import { ErrorBoundary } from '@/blocks/errorhandling' function CustomFallback({ error, errorId, reset }) { return (

{error.message}

) } export default function Page() { return ( ) } ``` ## Notes - The file has `'use client'` at the top because `ErrorBoundary` is a class component — `logger`, the error classes, `handleApiError`, `withErrorHandler`, and `tryCatch` are all runtime-safe on the server, but you'll need to import them carefully if your bundler enforces client/server boundaries; split into two files if that's a problem - The file contains two declarations of `AppError` and `handleApiError` — the second set (with `toJSON`, `ForbiddenError`, `ExternalServiceError`, `withErrorHandler`) supersedes the first; remove the first block before shipping to avoid a TypeScript duplicate identifier error - `ValidationError` accepts a `fields` map (`{ email: 'Already taken' }`) but `handleApiError` serializes it via `toJSON()` which only includes `error`, `code`, `statusCode`, `context` — surface the `fields` to clients by passing them as `context` or by checking `err instanceof ValidationError` before calling `handleApiError` - `fetchWithTimeout` defaults to a 10-second timeout; pass `{ timeoutMs: 5000 }` to tighten it per-call