# 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 (
<div>
<p>{error.message}</p>
<button onClick={reset}>Retry</button>
</div>
)
}
export default function Page() {
return (
<ErrorBoundary fallback={CustomFallback}>
<RiskyComponent />
</ErrorBoundary>
)
}
```
## 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