Most developers treat error handling as an afterthought — a try/catch wrapped around the happy path. Production-grade error handling is a system: typed errors, structured logging, user-facing messages, and automated alerting that catches issues before users report them.
Typed Error Classes
Define your error taxonomy upfront:
// lib/errors.ts
export class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number = 500,
public readonly context?: Record<string, unknown>
) {
super(message)
this.name = this.constructor.name
Error.captureStackTrace(this, this.constructor)
}
}
export class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(`${resource} not found`, 'NOT_FOUND', 404, { resource, id })
}
}
export class ValidationError extends AppError {
constructor(message: string, public readonly fields: Record<string, string[]>) {
super(message, 'VALIDATION_ERROR', 422, { fields })
}
}
export class UnauthorizedError extends AppError {
constructor(message = 'Authentication required') {
super(message, 'UNAUTHORIZED', 401)
}
}
export class RateLimitError extends AppError {
constructor(public readonly retryAfter: number) {
super('Rate limit exceeded', 'RATE_LIMITED', 429, { retryAfter })
}
}
Centralized Error Handler
// app/api/error-handler.ts
import { AppError } from '@/lib/errors'
import { logger } from '@/lib/logger'
export function handleApiError(error: unknown): Response {
if (error instanceof AppError) {
// Expected errors -- log at warn level
logger.warn({
code: error.code,
message: error.message,
context: error.context,
})
return Response.json(
{ error: error.message, code: error.code, context: error.context },
{ status: error.statusCode }
)
}
// Unexpected errors -- log at error level with full stack
const id = crypto.randomUUID()
logger.error({
errorId: id,
message: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
})
return Response.json(
{ error: 'Internal server error', errorId: id },
{ status: 500 }
)
}
Using the Handler in API Routes
// app/api/users/[id]/route.ts
import { handleApiError } from '@/api/error-handler'
import { NotFoundError, UnauthorizedError } from '@/lib/errors'
export async function GET(req: Request, { params }: { params: { id: string } }) {
try {
const session = await getServerSession()
if (!session) throw new UnauthorizedError()
const user = await db.user.findUnique({ where: { id: params.id } })
if (!user) throw new NotFoundError('User', params.id)
return Response.json(user)
} catch (error) {
return handleApiError(error)
}
}
Structured Logging
// lib/logger.ts
import pino from 'pino'
export const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
...(process.env.NODE_ENV === 'development' && {
transport: { target: 'pino-pretty' },
}),
base: {
service: 'api',
environment: process.env.NODE_ENV,
},
})
Log structured JSON in production. Every log entry should include a requestId, userId when available, and duration for slow operations.
Client-Side Error Boundaries
// app/error.tsx -- Next.js error boundary
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Report to error tracking
console.error(error)
}, [error])
return (
<div className='flex flex-col items-center justify-center min-h-screen'>
<h2 className='text-xl font-semibold mb-4'>Something went wrong</h2>
{error.digest && (
<p className='text-sm text-gray-500 mb-4'>Error ID: {error.digest}</p>
)}
<button onClick={reset} className='btn-primary'>Try again</button>
</div>
)
}
Alerting on Error Spikes
Log error rates to your analytics and alert when they exceed baseline:
// In your error handler
if (statusCode >= 500) {
await redis.incr(`errors:5xx:${Math.floor(Date.now() / 60000)}`)
// n8n or webhook checks this counter every 5 min
// alerts if count > threshold
}
The AI SaaS Starter at whoffagents.com ships with typed error classes, centralized API error handling, pino structured logging, and Next.js error boundaries pre-configured. $99 one-time.
Top comments (0)