$ npx rulesync-cli pull✓ Wrote CLAUDE.md (2 rulesets)# Coding Standards- Always use async/await- Prefer named exports
Best Practices

Error Handling Patterns for AI-Generated Code

AI swallows errors, catches too broadly, and logs nothing useful. Rules for structured errors, proper catch scopes, error boundaries, and operational error handling.

8 min read·July 9, 2024

catch (err) { console.error(err); return null; } — the most dangerous AI pattern

Typed errors, narrow catches, error boundaries, retry patterns, and structured logging

How AI Handles Errors (And Why Every Pattern Is Wrong)

AI generates three error handling anti-patterns consistently: catch-all (catching Exception or Error and swallowing it), log-and-continue (catching, logging console.error, and returning a default value as if nothing happened), and no-handling (no try/catch at all — unhandled errors crash the process). All three produce code that silently corrupts state, loses error context, or crashes unpredictably.

Good error handling is nuanced: some errors should crash the process (programmer bugs — assertion failures, type errors), some should be caught and retried (transient failures — network timeouts, database deadlocks), some should be caught and returned to the user (validation errors, not-found), and some should be caught and logged for ops (unexpected server errors). AI treats all errors the same — either ignoring them or catching everything.

These rules define which errors to catch, how to catch them, what to do with them, and how to communicate them. They apply to any language — the patterns are universal, though the syntax varies.

Rule 1: Typed Error Hierarchies

The rule: 'Define a custom error hierarchy for your application. Base: AppError with message, code, statusCode. Subclasses: ValidationError (400, field-level errors), NotFoundError (404, resource type + ID), AuthenticationError (401), AuthorizationError (403), ConflictError (409), InternalError (500, wraps unexpected errors). Throw specific types — catch specific types. Never throw generic Error or string.'

For the base class: 'class AppError extends Error { constructor(public message: string, public statusCode: number, public code: string) { super(message); this.name = this.constructor.name; } }. Subclasses add domain-specific data: class ValidationError extends AppError { constructor(public fields: Record<string, string>) { super("Validation failed", 400, "VALIDATION_ERROR"); } }.'

AI throws new Error("Something went wrong") — generic, untyped, and useless for programmatic handling. Typed errors let catch blocks handle different cases differently: catch ValidationError → return 400 with field errors. catch NotFoundError → return 404. catch unknown → log + return 500.

  • AppError base: message, statusCode, code — extends native Error
  • ValidationError: 400, field-level errors — catch and return to user
  • NotFoundError: 404, resource type + ID — catch and return to user
  • AuthError: 401/403 — catch and redirect/deny
  • InternalError: 500, wraps unexpected — catch, log details, return generic message
ℹ️ Typed Errors = Programmatic Handling

throw new Error('Something went wrong') is useless for catch blocks. throw new NotFoundError('User', id) lets the handler return 404 with context. Typed errors enable different handling for different cases.

Rule 2: Narrow Catch Scopes — Never Catch Everything

The rule: 'Catch specific error types, not generic Error or Exception. try { ... } catch (err) { if (err instanceof ValidationError) { return res.status(400).json(err.fields); } if (err instanceof NotFoundError) { return res.status(404).json({ message: err.message }); } throw err; // Re-throw unexpected errors — dont swallow them }. Unexpected errors propagate to the global error handler — they are NOT silently caught.'

For the golden rule: 'If you catch it, you must handle it meaningfully. Meaningful handling is: returning an appropriate response to the user, retrying the operation, or logging and re-throwing. console.error(err) by itself is NOT meaningful handling — you have logged the error but the caller still gets a broken response or no response. Log + return error response = meaningful. Log alone = silent failure.'

AI generates catch (err) { console.error(err); return null; } — the error is logged but the caller receives null instead of a meaningful error response. Every consumer of this function now has an unexpected null to handle. The error cascades as a different, harder-to-debug problem.

⚠️ Log Alone = Silent Failure

console.error(err) logs the error but the caller receives null or undefined. The error cascades as a different, harder-to-debug problem. Log + return error response = meaningful handling. Log alone = silent failure.

Rule 3: Error Boundaries at Architecture Layers

The rule: 'Place error boundaries at architectural layers: HTTP handler boundary (catches all errors from the service layer, maps to HTTP responses), service boundary (catches database/external errors, wraps in domain errors), and UI boundary (React ErrorBoundary or framework equivalent, shows fallback UI). Each boundary catches errors from the layer below and translates them for the layer above.'

For the HTTP boundary: 'A centralized error handler middleware: app.use((err, req, res, next) => { if (err instanceof AppError) { return res.status(err.statusCode).json({ error: { message: err.message, code: err.code } }); } logger.error({ err, requestId: req.id }); return res.status(500).json({ error: { message: "Internal server error", code: "INTERNAL" } }); }). One place for all error-to-response mapping.'

For the UI boundary: 'React ErrorBoundary catches render errors — shows a fallback UI instead of a white screen. Place at: app root (catches everything), feature sections (isolates failures), and individual components (for unreliable third-party widgets). Each boundary logs the error and displays an appropriate fallback.'

Rule 4: Retry Patterns for Transient Failures

The rule: 'Retry transient errors (network timeout, database deadlock, rate limit 429) with exponential backoff: wait 1s, 2s, 4s, 8s between retries. Maximum 3-5 retries. Never retry: validation errors (they will fail again), auth errors (credentials are wrong), not-found (the resource does not exist). Implement with a retry helper: async function withRetry<T>(fn: () => Promise<T>, maxRetries: number = 3): Promise<T>.'

For classification: 'Transient (retry): ECONNRESET, ETIMEDOUT, HTTP 429, HTTP 503, database deadlock, DNS ENOTFOUND (temporary). Permanent (do not retry): HTTP 400, 401, 403, 404, 409, validation errors, type errors, assertion failures. If unsure, do not retry — retrying a permanent error wastes resources and delays the error response.'

AI retries everything or retries nothing. The classification is the key: transient errors (external system temporarily unavailable) should be retried. Permanent errors (your request is wrong) should not. One retry helper with a classification function handles both cases correctly.

  • Retry: network timeout, 429, 503, deadlock — transient, external failures
  • Never retry: 400, 401, 403, 404, validation — permanent, request is wrong
  • Exponential backoff: 1s, 2s, 4s, 8s — max 3-5 retries
  • Retry helper: withRetry(fn, { maxRetries, isRetryable }) — reusable
  • Circuit breaker: after N failures, stop trying for a cooldown period
💡 Classify Before Retrying

Transient (retry): network timeout, 429, 503, deadlock. Permanent (never retry): 400, 401, 404, validation. Retrying a permanent error wastes resources and delays the error response. One classification function prevents both.

Rule 5: Structured Error Logging

The rule: 'Log errors as structured JSON with context: logger.error({ error: { name: err.name, message: err.message, code: err.code, stack: err.stack }, request: { id: req.id, method: req.method, path: req.path, userId: req.user?.id }, timestamp: new Date().toISOString() }). Never console.error(err) — it produces unstructured output that is unsearchable. Use a structured logger: pino (Node.js), structlog (Python), zerolog (Go).'

For what to log: 'Log: error type, error message, stack trace, request ID, user ID, timestamp, and any context needed to reproduce the issue. Never log: passwords, tokens, full request bodies with PII, credit card numbers. Sanitize before logging — use an allowlist of fields, not a denylist.'

For alerting: 'Log-based alerting: error rate > threshold → PagerDuty. New error type (never seen before) → Slack. Error rate spike (50% increase) → email. Connect structured logs to your alerting system — unstructured console.error cannot be alertd on reliably.'

Complete Error Handling Rules Template

Consolidated rules for error handling in any language.

  • Typed error hierarchy: AppError → ValidationError, NotFoundError, AuthError, InternalError
  • Catch specific types — never generic Exception — re-throw unexpected errors
  • Meaningful handling: return response OR retry OR log+rethrow — never log alone
  • Error boundaries at layers: HTTP handler, service, UI — translate between layers
  • Retry transient errors with exponential backoff — never retry permanent errors
  • Structured JSON logging: error type + context + requestId — never console.error
  • Sanitize before logging: no passwords, tokens, PII, card numbers
  • Alerting: error rate threshold, new error types, rate spikes — structured logs enable this
Error Handling Patterns for AI-Generated Code — RuleSync Blog