Utility · Starter
Error Handling
Structured logging, typed custom errors, a React Error Boundary, and API route wrappers — gives every thrown error a consistent shape and HTTP status code so your app fails predictably.
Read the Getting Access guideif you haven't yet.
1. Get the file
Sign in at marrowstack.dev, open the Error Handling block, and click Copy all files. Paste lib/errors.ts (and the React components) into your project. No runtime dependencies — the block uses only TypeScript and React built-ins.
2. Prerequisites
- Next.js 14 or 15, App Router
- TypeScript 5+ with strict mode
- React 18+ (for the Error Boundary component)
3. Install
No additional packages required. The block uses only Next.js and React built-ins.
4. Wire it in
Throw AppError anywhere in your server code — withApi() catches it and maps it to the correct HTTP status automatically:
import { withApi, AppError } from '@/lib/errors'
import { NextRequest } from 'next/server'
export const GET = withApi(async (req: NextRequest, { params }) => {
const item = await db.items.findUnique({ where: { id: params.id } })
if (!item) throw new AppError('Item not found', 'NOT_FOUND')
if (!item.isPublic && !isOwner(item, req)) {
throw new AppError('Forbidden', 'FORBIDDEN')
}
return Response.json(item)
})// AppError maps these codes to HTTP status codes automatically:
// 'NOT_FOUND' → 404
// 'FORBIDDEN' → 403
// 'UNAUTHORIZED' → 401
// 'VALIDATION_ERROR' → 422
// 'CONFLICT' → 409
// 'TOO_MANY_REQUESTS' → 429
// 'INTERNAL_ERROR' → 500
throw new AppError('Email already in use', 'CONFLICT')
throw new AppError('Invalid input', 'VALIDATION_ERROR', { field: 'email' })
// ^ extra context, logged but not sent to client'use client'
import { ErrorBoundary, ErrorFallback } from '@/lib/errors'
// Wrap any subtree — unhandled render errors show ErrorFallback instead of crashing the page
export function ItemsSection() {
return (
<ErrorBoundary fallback={<ErrorFallback message="Could not load items." />}>
<ItemsList />
</ErrorBoundary>
)
}import { safeLog } from '@/lib/errors'
// Logs to console in dev, structured JSON in production (works with Vercel Log Drains)
safeLog('info', 'Payment captured', { orderId, amount })
safeLog('warn', 'Webhook retry detected', { attempt: 3 })
safeLog('error', 'Supabase insert failed', { error, table: 'ms_users' })5. Verify it works
- Throw
new AppError('Test', 'NOT_FOUND')in a route wrapped withwithApi()and confirm the response is404with a JSON body. - Throw an unhandled
Error(not anAppError) and confirmwithApi()returns500without leaking the stack trace to the client. - Wrap a component that throws during render with
ErrorBoundaryand confirm the fallback UI is shown. - Call
safeLog('error', 'test', { foo: 1 })and confirm it appears in your terminal / Vercel logs.
6. Failure modes & fixes
withApi() returns 500 for all AppErrors
Cause: The error code is not one of the built-in codes, so it defaults to 500.
Fix: Use one of the defined codes: NOT_FOUND, FORBIDDEN, UNAUTHORIZED, VALIDATION_ERROR, CONFLICT, TOO_MANY_REQUESTS, INTERNAL_ERROR. Or add custom codes to the statusMap in errors.ts.
ErrorBoundary not catching errors
Cause: ErrorBoundary only catches errors during React rendering, not in async event handlers or setTimeout.
Fix: For async errors (e.g. fetch in a click handler), handle them with try/catch and set local error state. ErrorBoundary is for render-phase failures only.
safeLog output missing in Vercel logs
Cause: Log output is buffered and may not flush before the serverless function exits.
Fix: Ensure safeLog is called before returning from the handler. For critical logs, await the log write before responding.