DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Production Error Handling in Next.js: Typed Errors, Structured Logging, and Alerting

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 })
  }
}
Enter fullscreen mode Exit fullscreen mode

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 }
  )
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
  },
})
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)