Skip to content

Error Handling Patterns

This document outlines patterns for implementing error handling in the ReX content system. Effective error handling provides better diagnostics, recovery options, and a more resilient content management experience.

Error Hierarchy Pattern

Pattern Overview

The Error Hierarchy pattern creates a consistent structure of error types for better error identification and handling.

Implementation Example

typescript
// Base error class for all content system errors
export class ContentError extends Error {
  // Properties to enhance error information
  public readonly uri?: string
  public readonly operation?: string
  public readonly cause?: Error
  public readonly recoverable: boolean
  public context: Record<string, any>

  constructor(
    message: string,
    options: {
      uri?: string
      operation?: string
      cause?: Error
      recoverable?: boolean
      context?: Record<string, any>
    } = {}
  ) {
    super(message)

    // Set name to the constructor name for better debugging
    this.name = this.constructor.name

    // Capture stack trace
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, this.constructor)
    }

    // Set additional properties
    this.uri = options.uri
    this.operation = options.operation
    this.cause = options.cause
    this.recoverable = options.recoverable ?? true
    this.context = options.context ?? {}
  }

  // Get full error message including context
  getFullMessage(): string {
    let message = this.message

    if (this.uri) {
      message += ` (URI: ${this.uri})`
    }

    if (this.operation) {
      message += ` during ${this.operation} operation`
    }

    if (this.cause) {
      message += `\nCaused by: ${this.cause.message}`
    }

    return message
  }

  // Get recovery suggestions if applicable
  getRecoverySuggestions(): string[] {
    return []
  }
}

// Specific error types for different scenarios

// Content not found
export class ContentNotFoundError extends ContentError {
  constructor(uri: string, options: {} = {}) {
    super(`Content not found at "${uri}"`, {
      uri,
      ...options,
      recoverable: false,
    })
  }

  getRecoverySuggestions(): string[] {
    return [
      'Verify the URI is correct',
      'Check if the content exists in the storage',
      'Ensure you have read permissions for the content',
    ]
  }
}

// Content access error (permissions, quota, etc.)
export class ContentAccessError extends ContentError {
  constructor(message: string, options: {} = {}) {
    super(message, {
      ...options,
      recoverable: false,
    })
  }

  getRecoverySuggestions(): string[] {
    return [
      'Check if you have appropriate permissions',
      'Verify storage quotas are not exceeded',
      'Ensure the content is not locked by another process',
    ]
  }
}

// Content validation error
export class ContentValidationError extends ContentError {
  public readonly validationErrors: any[]

  constructor(
    message: string,
    options: {
      validationErrors?: any[]
      [key: string]: any
    } = {}
  ) {
    super(message, options)
    this.validationErrors = options.validationErrors || []
  }

  getRecoverySuggestions(): string[] {
    return [
      'Fix the content format according to validation rules',
      'Check the content structure matches the expected schema',
      'Ensure all required fields are provided',
    ]
  }
}

// Content format error
export class ContentFormatError extends ContentError {
  constructor(message: string, options: {} = {}) {
    super(message, options)
  }

  getRecoverySuggestions(): string[] {
    return [
      'Verify the content format matches the specified contentType',
      'Check for syntax errors in the content',
      'Ensure the content encoding is correct',
    ]
  }
}

// Storage operation error
export class StorageError extends ContentError {
  constructor(message: string, options: {} = {}) {
    super(message, {
      ...options,
      recoverable: true,
    })
  }

  getRecoverySuggestions(): string[] {
    return [
      'Retry the operation after a short delay',
      'Check the storage connection status',
      'Verify the storage is not in maintenance mode',
    ]
  }
}

Considerations

  • Create a logical hierarchy that reflects the system’s error domains
  • Include enough context for effective debugging and user feedback
  • Make error messages clear and actionable
  • Indicate whether errors are recoverable and how recovery might be attempted

Error Context Pattern

Pattern Overview

The Error Context pattern enriches errors with additional information to aid debugging and user feedback.

Implementation Example

typescript
import { ContentError } from '@lib/errors/content'

// Function to enrich error with context
export function enrichErrorWithContext(
  error: Error,
  context: Record<string, any>
): Error {
  // If already a ContentError, add to its context
  if (error instanceof ContentError) {
    error.context = {
      ...error.context,
      ...context,
    }
    return error
  }

  // Otherwise, wrap in a ContentError
  return new ContentError(error.message, {
    cause: error,
    context,
  })
}

// Middleware that adds operation context to errors
export function withErrorContext(context: Record<string, any>) {
  return next => async params => {
    try {
      // Add request-specific context
      const requestContext = {
        ...context,
        uri: params.uri,
        operation: params.type,
        timestamp: new Date().toISOString(),
        requestId: generateRequestId(),
      }

      // Execute operation
      return await next(params)
    } catch (error) {
      // Enrich error with context
      throw enrichErrorWithContext(error, requestContext)
    }
  }
}

// Usage example
const enhancedOperation = pipe(
  baseOperation,
  withErrorContext({
    component: 'ContentStore',
    environment: getEnvironmentInfo(),
  })
)

// Helper to generate request ID
function generateRequestId() {
  return Math.random().toString(36).substring(2, 15)
}

// Helper to get environment info
function getEnvironmentInfo() {
  return {
    platform: typeof window !== 'undefined' ? 'browser' : 'node',
    version: process.env.VERSION || 'unknown',
  }
}

Considerations

  • Include only relevant information in the context
  • Avoid sensitive data in error contexts (credentials, personal data)
  • Structure context consistently across different operations
  • Consider context size to prevent excessive memory usage

Error Recovery Pattern

Pattern Overview

The Error Recovery pattern implements strategies for recovering from errors and continuing operations when possible.

Implementation Example

typescript
import { ContentError } from '@lib/errors/content'

// Retry failed operations
export function withRetry(options = {}) {
  const {
    maxRetries = 3,
    delay = 1000,
    backoff = 2,
    retryableErrors = [StorageError],
  } = options

  return next => async params => {
    let lastError

    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        // Execute operation
        return await next(params)
      } catch (error) {
        lastError = error

        // Check if error is retryable
        const isRetryable =
          (error instanceof ContentError && error.recoverable) ||
          retryableErrors.some(ErrorType => error instanceof ErrorType)

        // Stop retrying if not retryable or max retries reached
        if (!isRetryable || attempt >= maxRetries) {
          break
        }

        // Calculate backoff delay
        const retryDelay = delay * Math.pow(backoff, attempt)

        // Wait before retrying
        await new Promise(resolve => setTimeout(resolve, retryDelay))
      }
    }

    // If we get here, all retries failed
    throw lastError
  }
}

// Fallback to alternative implementation
export function withFallback(fallbackFn) {
  return next => async params => {
    try {
      // Try primary implementation
      return await next(params)
    } catch (error) {
      // Log the primary error
      console.warn(`Primary implementation failed: ${error.message}`)

      // Try fallback implementation
      return await fallbackFn(params)
    }
  }
}

// Circuit breaker to prevent cascading failures
export function withCircuitBreaker(options = {}) {
  const {
    failureThreshold = 5,
    resetTimeout = 30000,
    halfOpenLimit = 1,
  } = options

  // Circuit state
  let failures = 0
  let circuitOpen = false
  let lastFailure = 0
  let halfOpenExecutions = 0

  return next => async params => {
    // Check if circuit is open
    if (circuitOpen) {
      const now = Date.now()

      // Check if we can try half-open state
      if (now - lastFailure >= resetTimeout) {
        // Allow limited traffic in half-open state
        if (halfOpenExecutions >= halfOpenLimit) {
          throw new ContentError('Circuit breaker is open', {
            context: {
              circuitState: 'open',
              lastFailure: new Date(lastFailure).toISOString(),
              failures,
            },
          })
        }

        halfOpenExecutions++
      } else {
        throw new ContentError('Circuit breaker is open', {
          context: {
            circuitState: 'open',
            lastFailure: new Date(lastFailure).toISOString(),
            failures,
            resetIn: resetTimeout - (now - lastFailure),
          },
        })
      }
    }

    try {
      // Execute operation
      const result = await next(params)

      // Success - reset circuit if in half-open state
      if (circuitOpen) {
        circuitOpen = false
        failures = 0
        halfOpenExecutions = 0
      }

      return result
    } catch (error) {
      // Record failure
      failures++
      lastFailure = Date.now()

      // Check if threshold reached
      if (failures >= failureThreshold) {
        circuitOpen = true
      }

      // Re-throw the original error
      throw error
    }
  }
}

// Usage example
const robustOperation = pipe(
  baseOperation,
  withRetry({
    maxRetries: 3,
    retryableErrors: [StorageError, NetworkError],
  }),
  withFallback(fallbackImplementation),
  withCircuitBreaker({ failureThreshold: 5 })
)

Considerations

  • Define clear criteria for retryable vs. non-retryable errors
  • Implement progressive backoff to prevent overwhelming failed services
  • Create effective circuit breakers to prevent cascading failures
  • Log recovery attempts for monitoring and debugging

Error Logging Pattern

Pattern Overview

The Error Logging pattern provides structured error logging for easier troubleshooting and monitoring.

Implementation Example

typescript
import { ContentError } from '@lib/errors/content'
import { Logger } from '@lib/telemetry/logger'

// Create middleware for error logging
export function withErrorLogging(options = {}) {
  const {
    logger = Logger.getInstance(),
    level = 'error',
    includeStackTrace = true,
    includeCause = true,
  } = options

  return next => async params => {
    try {
      // Execute operation
      return await next(params)
    } catch (error) {
      // Prepare log data
      const logData = {
        uri: params.uri,
        operation: params.type,
        errorName: error.name || 'Error',
        errorMessage: error.message,
        timestamp: new Date().toISOString(),
      }

      // Add stack trace if requested
      if (includeStackTrace && error.stack) {
        logData.stack = error.stack
      }

      // Add error context if available
      if (error instanceof ContentError) {
        logData.context = error.context
        logData.recoverable = error.recoverable

        // Add cause if requested
        if (includeCause && error.cause) {
          logData.cause = {
            name: error.cause.name,
            message: error.cause.message,
            stack: includeStackTrace ? error.cause.stack : undefined,
          }
        }
      }

      // Log the error
      logger.log(level, `Content error: ${error.message}`, logData)

      // Re-throw the original error
      throw error
    }
  }
}

// Create error boundary for unhandled errors
export function createErrorBoundary(operation, options = {}) {
  const { logger = Logger.getInstance(), onError = null } = options

  return async params => {
    try {
      // Execute operation
      return await operation(params)
    } catch (error) {
      // Log unhandled error
      logger.error('Unhandled content error', {
        uri: params.uri,
        operation: params.type,
        error: {
          name: error.name,
          message: error.message,
          stack: error.stack,
        },
      })

      // Call custom error handler if provided
      if (onError) {
        await onError(error, params)
      }

      // Return error result
      return {
        error: {
          message: error.message,
          type: error.name,
          recoverable:
            error instanceof ContentError ? error.recoverable : false,
        },
      }
    }
  }
}

// Usage example
const loggingOperation = pipe(
  baseOperation,
  withErrorLogging({
    level: 'error',
    includeStackTrace: process.env.NODE_ENV !== 'production',
  })
)

const safeBoundary = createErrorBoundary(loggingOperation, {
  onError: async (error, params) => {
    // Send error to monitoring service
    await errorMonitoring.report(error)

    // Notify administrators if critical
    if (isCriticalOperation(params)) {
      await notifyAdmins(error, params)
    }
  },
})

Considerations

  • Structure logs consistently for easier parsing and analysis
  • Include enough context for troubleshooting without exposing sensitive data
  • Consider performance impact of verbose logging in production
  • Implement log levels to control level of detail

Error Translation Pattern

Pattern Overview

The Error Translation pattern converts low-level technical errors into domain-specific errors with user-friendly messages.

Implementation Example

typescript
import {
  ContentError,
  ContentNotFoundError,
  ContentAccessError,
} from '@lib/errors/content'

// Function to translate technical errors to domain errors
export function translateError(error, context = {}) {
  // Already a domain error - pass through
  if (error instanceof ContentError) {
    return error
  }

  // Translation logic based on error type and properties

  // File system errors
  if (error.code === 'ENOENT') {
    return new ContentNotFoundError(context.uri || 'unknown', {
      cause: error,
      context,
    })
  }

  if (error.code === 'EACCES' || error.code === 'EPERM') {
    return new ContentAccessError(`Permission denied for content operation`, {
      cause: error,
      context,
    })
  }

  // Database errors
  if (error.name === 'NotFoundError' || error.status === 404) {
    return new ContentNotFoundError(context.uri || 'unknown', {
      cause: error,
      context,
    })
  }

  if (error.name === 'UnauthorizedError' || error.status === 403) {
    return new ContentAccessError(`Unauthorized content access`, {
      cause: error,
      context,
    })
  }

  // Network errors
  if (error.name === 'NetworkError' || error.message?.includes('network')) {
    return new StorageError(`Network error during content operation`, {
      cause: error,
      context,
      recoverable: true,
    })
  }

  // Default - wrap in generic ContentError
  return new ContentError(`Error during content operation: ${error.message}`, {
    cause: error,
    context,
  })
}

// Middleware for error translation
export function withErrorTranslation(context = {}) {
  return next => async params => {
    try {
      // Execute operation
      return await next(params)
    } catch (error) {
      // Translate error with operation context
      const operationContext = {
        ...context,
        uri: params.uri,
        operation: params.type,
      }

      throw translateError(error, operationContext)
    }
  }
}

// User-friendly error messages
export function getUserFriendlyMessage(error) {
  if (error instanceof ContentNotFoundError) {
    return `The requested content could not be found. Please check the address and try again.`
  }

  if (error instanceof ContentAccessError) {
    return `You don't have permission to access this content. Please contact an administrator if you need access.`
  }

  if (error instanceof ContentValidationError) {
    return `The content format is invalid. Please check the content structure and try again.`
  }

  if (error instanceof StorageError) {
    return `A temporary storage issue occurred. Please try again in a few moments.`
  }

  // Default message
  return `An unexpected error occurred. Please try again or contact support if the problem persists.`
}

// Usage example
try {
  await contentStore.read('articles/missing-article.md')
} catch (error) {
  // Display user-friendly message
  console.error(getUserFriendlyMessage(error))

  // Log technical details
  console.debug('Technical error details:', error)
}

Considerations

  • Map technical errors consistently to domain-specific errors
  • Preserve original error as cause for debugging
  • Make user-facing messages helpful without exposing implementation details
  • Consider internationalization for user-facing messages

Command Pattern for Error Handling

Pattern Overview

The Command pattern encapsulates operations as objects that can be executed, logged, and recovered uniformly.

Implementation Example

typescript
// Command interface
interface Command<T> {
  execute(): Promise<T>
  getDescription(): string
  rollback?(): Promise<void>
}

// Content read command
class ReadContentCommand implements Command<ContentData> {
  private adapter: ContentAdapter
  private uri: string
  private options: ReadOptions

  constructor(adapter: ContentAdapter, uri: string, options: ReadOptions = {}) {
    this.adapter = adapter
    this.uri = uri
    this.options = options
  }

  async execute(): Promise<ContentData> {
    try {
      return await this.adapter.read(this.uri)
    } catch (error) {
      // Translate the error
      throw translateError(error, {
        uri: this.uri,
        operation: 'read',
        options: this.options,
      })
    }
  }

  getDescription(): string {
    return `Read content from "${this.uri}"`
  }
}

// Content write command with rollback capability
class WriteContentCommand implements Command<void> {
  private adapter: ContentAdapter
  private uri: string
  private content: ContentData
  private options: WriteOptions
  private previousContent: ContentData | null = null

  constructor(
    adapter: ContentAdapter,
    uri: string,
    content: ContentData,
    options: WriteOptions = {}
  ) {
    this.adapter = adapter
    this.uri = uri
    this.content = content
    this.options = options
  }

  async execute(): Promise<void> {
    try {
      // Store previous content for rollback if needed
      try {
        this.previousContent = await this.adapter.read(this.uri)
      } catch (error) {
        // Content doesn't exist - rollback will delete
        this.previousContent = null
      }

      // Write new content
      await this.adapter.write(this.uri, this.content)
    } catch (error) {
      // Translate the error
      throw translateError(error, {
        uri: this.uri,
        operation: 'write',
        options: this.options,
      })
    }
  }

  async rollback(): Promise<void> {
    if (this.previousContent === null) {
      // Content didn't exist before - delete it
      try {
        await this.adapter.delete(this.uri)
      } catch (error) {
        // Log rollback failure
        console.error(`Rollback failed for ${this.uri}:`, error)
      }
    } else {
      // Restore previous content
      try {
        await this.adapter.write(this.uri, this.previousContent)
      } catch (error) {
        // Log rollback failure
        console.error(`Rollback failed for ${this.uri}:`, error)
      }
    }
  }

  getDescription(): string {
    return `Write content to "${this.uri}"`
  }
}

// Command executor with error handling
class CommandExecutor {
  private logger: Logger

  constructor(logger: Logger) {
    this.logger = logger
  }

  async execute<T>(command: Command<T>): Promise<T> {
    this.logger.debug(`Executing: ${command.getDescription()}`)

    try {
      // Execute the command
      const result = await command.execute()

      this.logger.debug(`Successfully executed: ${command.getDescription()}`)

      return result
    } catch (error) {
      this.logger.error(`Failed to execute: ${command.getDescription()}`, {
        error: error.message,
        stack: error.stack,
      })

      // Re-throw the error
      throw error
    }
  }

  async executeTransaction(commands: Command<any>[]): Promise<any[]> {
    const results: any[] = []
    const executedCommands: Command<any>[] = []

    try {
      // Execute all commands
      for (const command of commands) {
        const result = await this.execute(command)
        results.push(result)
        executedCommands.push(command)
      }

      return results
    } catch (error) {
      // Roll back executed commands in reverse order
      for (let i = executedCommands.length - 1; i >= 0; i--) {
        const command = executedCommands[i]

        if (command.rollback) {
          try {
            this.logger.debug(`Rolling back: ${command.getDescription()}`)
            await command.rollback()
          } catch (rollbackError) {
            this.logger.error(
              `Rollback failed for: ${command.getDescription()}`,
              {
                error: rollbackError.message,
                originalError: error.message,
              }
            )
          }
        }
      }

      // Re-throw the original error
      throw error
    }
  }
}

// Usage example
const executor = new CommandExecutor(logger)

// Single command
try {
  const content = await executor.execute(
    new ReadContentCommand(adapter, 'articles/article-1.md')
  )
  // Use content
} catch (error) {
  // Handle error
}

// Transaction with multiple commands
try {
  const results = await executor.executeTransaction([
    new ReadContentCommand(adapter, 'articles/article-1.md'),
    new WriteContentCommand(adapter, 'articles/article-1-backup.md', content),
    new WriteContentCommand(adapter, 'articles/article-1.md', updatedContent),
  ])
  // Use results
} catch (error) {
  // Transaction failed and was rolled back
}

Considerations

  • Implement rollback operations for transactional consistency
  • Log command execution and rollbacks for auditability
  • Design commands with clear responsibilities and boundaries
  • Consider resource usage for long-running commands

Event Emitter Pattern

Pattern Overview

The Event Emitter pattern provides a standard way to notify components about errors without tight coupling.

Implementation Example

typescript
import { EventEmitter } from 'events'

// Create a global error event emitter
export const errorEvents = new EventEmitter()

// Define event types
export const ERROR_EVENTS = {
  CONTENT_ERROR: 'content:error',
  STORAGE_ERROR: 'storage:error',
  VALIDATION_ERROR: 'validation:error',
  RECOVERABLE_ERROR: 'recoverable:error',
  CRITICAL_ERROR: 'critical:error',
}

// Middleware that emits error events
export function withErrorEvents() {
  return next => async params => {
    try {
      // Execute operation
      return await next(params)
    } catch (error) {
      // Determine error type
      let eventType = ERROR_EVENTS.CONTENT_ERROR

      if (error instanceof ContentNotFoundError) {
        eventType = ERROR_EVENTS.CONTENT_ERROR
      } else if (error instanceof StorageError) {
        eventType = ERROR_EVENTS.STORAGE_ERROR
      } else if (error instanceof ContentValidationError) {
        eventType = ERROR_EVENTS.VALIDATION_ERROR
      }

      // Emit specific error event
      errorEvents.emit(eventType, {
        error,
        params,
        timestamp: Date.now(),
      })

      // Additionally emit recoverable/critical event
      if (error instanceof ContentError && error.recoverable) {
        errorEvents.emit(ERROR_EVENTS.RECOVERABLE_ERROR, {
          error,
          params,
          timestamp: Date.now(),
        })
      } else {
        errorEvents.emit(ERROR_EVENTS.CRITICAL_ERROR, {
          error,
          params,
          timestamp: Date.now(),
        })
      }

      // Re-throw the error
      throw error
    }
  }
}

// Set up error handlers
function setupErrorHandlers() {
  // Log all content errors
  errorEvents.on(ERROR_EVENTS.CONTENT_ERROR, ({ error, params }) => {
    console.error(`Content error for ${params.uri}:`, error.message)
  })

  // Report critical errors to monitoring service
  errorEvents.on(ERROR_EVENTS.CRITICAL_ERROR, ({ error, params }) => {
    errorMonitoring.report(error, {
      uri: params.uri,
      operation: params.type,
    })
  })

  // Try recovery for recoverable errors
  errorEvents.on(ERROR_EVENTS.RECOVERABLE_ERROR, ({ error, params }) => {
    console.warn(`Attempting recovery for ${params.uri}:`, error.message)
    attemptRecovery(error, params)
  })
}

// Usage example
const enhancedOperation = pipe(
  baseOperation,
  withErrorTranslation(),
  withErrorEvents()
)

// Set up listeners
setupErrorHandlers()

// Execute operation
try {
  const result = await enhancedOperation({
    uri: 'articles/article-1.md',
    type: 'read',
  })
  // Use result
} catch (error) {
  // Error already emitted, can add additional handling here
}

Considerations

  • Limit the number of event types to prevent event explosion
  • Document the event contract clearly for consumers
  • Consider memory leaks from forgotten listeners
  • Implement event batching for high-frequency errors

Released under the MIT License.