Skip to content

🚨 Error Handling ​

Handling errors gracefully without hiding bugs.

Principles ​

  • Fail fast β€” Detect errors as early as possible
  • Fail loud β€” Log errors with context, don't swallow them
  • Fail gracefully β€” Show users a helpful message, not a stack trace
  • Fail safely β€” Don't expose sensitive information in errors

Error handling patterns ​

Try-catch (use sparingly) ​

typescript
try {
  const user = await fetchUser(id)
} catch (error) {
  logger.error('Failed to fetch user', { userId: id, error })
  throw new AppError('USER_NOT_FOUND', 404)
}

Result type (preferred for business logic) ​

typescript
type Result<T> = { ok: true; data: T } | { ok: false; error: AppError }

Error boundaries (React) ​

Catch rendering errors at component level, show fallback UI.

Custom error classes ​

typescript
class AppError extends Error {
  constructor(
    public code: string,
    public statusCode: number,
    message?: string
  ) {
    super(message || code)
  }
}

What to log ​

  • Error message and stack trace
  • Request context (URL, method, user ID)
  • Input that caused the error
  • Timestamp and environment
  • Never log: passwords, tokens, PII

Error response to users ​

json
{
  "error": {
    "code": "PAYMENT_FAILED",
    "message": "Payment could not be processed. Please try again."
  }
}
  • User-facing messages: helpful, non-technical
  • Internal logs: detailed, technical, with context
  • Never expose stack traces in production responses

Anti-patterns ​

  • Empty catch blocks (catch (e) {})
  • Catching generic Error when you mean a specific type
  • Using exceptions for control flow
  • Returning null instead of throwing on unexpected failures
  • Logging without enough context to reproduce

Pergame Knowledge Base