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
// 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
Related Patterns
- Error Context Pattern - Enriching errors with contextual information
- Error Recovery Pattern - Strategies for recovering from errors
Error Context Pattern
Pattern Overview
The Error Context pattern enriches errors with additional information to aid debugging and user feedback.
Implementation Example
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
Related Patterns
- Error Logging Pattern - Capturing and recording error information
- Telemetry Pattern - Collecting error metrics for monitoring
Error Recovery Pattern
Pattern Overview
The Error Recovery pattern implements strategies for recovering from errors and continuing operations when possible.
Implementation Example
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
Related Patterns
- Bulkhead Pattern - Isolating components to prevent system-wide failures
- Cache Fallback Pattern - Using cached data when fresh data is unavailable
Error Logging Pattern
Pattern Overview
The Error Logging pattern provides structured error logging for easier troubleshooting and monitoring.
Implementation Example
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
Related Patterns
- Structured Logging Pattern - Consistent log format for easier analysis
- Correlation ID Pattern - Tracking related logs across operations
Error Translation Pattern
Pattern Overview
The Error Translation pattern converts low-level technical errors into domain-specific errors with user-friendly messages.
Implementation Example
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
Related Patterns
- Error Factory Pattern - Creating standardized errors
- User Feedback Pattern - Presenting errors to users effectively
Command Pattern for Error Handling
Pattern Overview
The Command pattern encapsulates operations as objects that can be executed, logged, and recovered uniformly.
Implementation Example
// 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
Related Patterns
- Unit of Work Pattern - Tracking changes for consistent commits
- Transaction Script Pattern - Organizing business logic in procedural scripts
Event Emitter Pattern
Pattern Overview
The Event Emitter pattern provides a standard way to notify components about errors without tight coupling.
Implementation Example
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
Related Patterns
- Observer Pattern - Broader pattern for general change notifications
- Pub/Sub Pattern - Decoupled messaging through intermediaries