Skip to content

Error Handling Guide

This guide explains effective error handling strategies for the content system. It covers error types, handling patterns, recovery strategies, and best practices.

Understanding the Error System

The content system uses a structured error hierarchy that provides rich context and recovery options.

Error Hierarchy

All errors in the system extend from ContentError:

ContentError (base error class)
├── ContentNotFoundError
├── ContentAccessError
├── ContentValidationError
├── ContentFormatError
├── ContentOperationError
│   ├── ContentReadError
│   ├── ContentWriteError
│   ├── ContentDeleteError
│   └── ContentListError
├── ContentAdapterError
│   ├── AdapterInitializationError
│   ├── AdapterOperationError
│   └── AdapterDisposeError
└── ContentRegistryError
    ├── StoreNotFoundError
    ├── AdapterNotFoundError
    └── PluginNotFoundError

Error Context

All errors provide rich context information:

typescript
// Base error properties
interface ContentError extends Error {
  // Basic information
  name: string
  message: string
  stack?: string

  // Context information
  uri?: string
  operation?: string
  source?: string

  // Recovery options
  recoverable: boolean
  recovery?: () => Promise<any>

  // Additional information
  cause?: Error
  info?: Record<string, any>
}

Basic Error Handling

Try-Catch Pattern

The simplest error handling approach:

typescript
try {
  // Attempt to read content
  const content = await store.read('articles/welcome.md')

  // Process content if read succeeds
  displayContent(content)
} catch (error) {
  // Handle the error
  console.error('Failed to read content:', error.message)

  // Show error message to user
  showErrorNotification('Could not load the article')
}

Error Type Checking

Handle specific error types differently:

typescript
try {
  await store.write('articles/draft.md', newContent)
} catch (error) {
  if (error instanceof ContentValidationError) {
    // Handle validation errors
    displayValidationErrors(error.info.validationErrors)
  } else if (error instanceof ContentAccessError) {
    // Handle permission errors
    promptForAuthentication()
  } else {
    // Handle other errors
    logError(error)
    showGenericErrorMessage()
  }
}

Error Properties

Access error properties for detailed handling:

typescript
try {
  await store.delete('articles/protected.md')
} catch (error) {
  if (error instanceof ContentError) {
    console.error(`Error in ${error.operation} operation on ${error.uri}`)
    console.error(`Source: ${error.source}`)

    if (error.cause) {
      console.error('Caused by:', error.cause)
    }

    if (error.info) {
      console.error('Additional info:', error.info)
    }
  }
}

Advanced Error Handling

Automatic Recovery

Use the recovery options provided by errors:

typescript
try {
  const content = await store.read('articles/cached.md')
  displayContent(content)
} catch (error) {
  if (error instanceof ContentError && error.recoverable && error.recovery) {
    try {
      // Attempt automatic recovery
      const recoveredContent = await error.recovery()
      displayContent(recoveredContent)
      showNotification('Using cached version')
    } catch (recoveryError) {
      // Recovery failed
      console.error('Recovery failed:', recoveryError)
      showErrorNotification('Could not load content, even from cache')
    }
  } else {
    // Unrecoverable error
    showErrorNotification('Could not load content')
  }
}

Error Boundaries

Create error boundaries for component-level handling:

typescript
class ContentErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // Log error details
    logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      const error = this.state.error;

      // Specific error handling based on type
      if (error instanceof ContentNotFoundError) {
        return <NotFoundMessage uri={error.uri} />;
      } else if (error instanceof ContentAccessError) {
        return <AccessDeniedMessage uri={error.uri} />;
      } else {
        return <GenericErrorMessage error={error} />;
      }
    }

    return this.props.children;
  }
}

// Usage
function ArticleView({ uri }) {
  return (
    <ContentErrorBoundary>
      <ContentDisplay uri={uri} />
    </ContentErrorBoundary>
  );
}

Fallback Content

Provide fallback content for errors:

typescript
async function loadContentWithFallback(uri) {
  try {
    // Attempt to load content
    return await store.read(uri)
  } catch (error) {
    if (error instanceof ContentNotFoundError) {
      // Return empty content with not found notice
      return {
        data: '# Not Found\n\nThe requested content could not be found.',
        contentType: 'text/markdown',
        metadata: { title: 'Not Found' },
      }
    } else if (error instanceof ContentAccessError) {
      // Return access denied notice
      return {
        data: '# Access Denied\n\nYou do not have permission to view this content.',
        contentType: 'text/markdown',
        metadata: { title: 'Access Denied' },
      }
    } else {
      // Log other errors and return generic error notice
      console.error('Error loading content:', error)
      return {
        data: '# Error\n\nAn error occurred while loading this content.',
        contentType: 'text/markdown',
        metadata: { title: 'Error' },
      }
    }
  }
}

Error Prevention Strategies

Content Validation

Validate content before writing:

typescript
import { validateContent } from '@lib/content/validation'

async function saveContent(uri, content) {
  // Validate content before attempting to write
  const validationResult = await validateContent(content, {
    schema: articleSchema,
    strict: true,
  })

  if (!validationResult.valid) {
    // Handle validation errors without throwing
    displayValidationErrors(validationResult.errors)
    return false
  }

  try {
    // Write pre-validated content
    await store.write(uri, content)
    return true
  } catch (error) {
    // Only network/permission errors should reach here
    handleOperationError(error)
    return false
  }
}

Existence Checking

Check if content exists before operations:

typescript
async function updateOrCreateContent(uri, content) {
  try {
    // Check if content exists
    const exists = await store.exists(uri)

    if (exists) {
      // Update existing content
      await store.write(uri, content, { overwrite: true })
      showNotification('Content updated')
    } else {
      // Create new content
      await store.write(uri, content, { createDirectories: true })
      showNotification('Content created')
    }

    return true
  } catch (error) {
    handleOperationError(error)
    return false
  }
}

Defensive Options

Use operation options to prevent errors:

typescript
// Prevent "not found" errors
const content = await store.read('articles/optional.md', {
  failIfNotFound: false,
  defaultContent: {
    data: '# Empty Article',
    contentType: 'text/markdown',
    metadata: { title: 'Empty Article' },
  },
})

// Prevent "already exists" errors
await store.write('articles/new.md', newContent, {
  failIfExists: false,
  overwrite: false,
})

// Prevent "not found" errors during delete
await store.delete('articles/maybe-exists.md', {
  failIfNotFound: false,
})

Error Handling Middleware

Creating Error Handling Middleware

Custom middleware for centralized error handling:

typescript
function errorHandlingMiddleware(options = {}) {
  return async (context, next) => {
    try {
      // Attempt the operation
      return await next()
    } catch (error) {
      // Log the error
      console.error(`Error in ${context.operation} operation:`, error)

      // Enrich error with context
      if (error instanceof ContentError) {
        error.operation = error.operation || context.operation
        error.uri = error.uri || context.uri
      }

      // Specific handling based on error type
      if (error instanceof ContentNotFoundError && options.provideFallback) {
        // Return fallback content
        context.content = getFallbackContent(context.uri)
        return context
      }

      // Rethrow to propagate to caller
      throw error
    }
  }
}

// Apply middleware to store
const store = createContentStore({
  adapter: createFileSystemAdapter({ basePath: '/content' }),
  middleware: [errorHandlingMiddleware({ provideFallback: true })],
})

Retry Middleware

Middleware that automatically retries failed operations:

typescript
function retryMiddleware(options = { maxRetries: 3, delay: 500 }) {
  return async (context, next) => {
    let attempts = 0

    while (true) {
      try {
        attempts++
        return await next()
      } catch (error) {
        // Check if we should retry
        const isRetryable =
          !(error instanceof ContentValidationError) && // Don't retry validation errors
          !(error instanceof ContentAccessError) && // Don't retry access errors
          attempts <= options.maxRetries

        if (!isRetryable) {
          // Add retry information to error
          if (error instanceof ContentError) {
            error.info = {
              ...error.info,
              retryAttempts: attempts,
            }
          }

          throw error
        }

        // Wait before retrying
        await new Promise(resolve => setTimeout(resolve, options.delay))

        // Exponential backoff
        options.delay *= 2
      }
    }
  }
}

Environment-Specific Error Handling

Node.js Error Handling

Error handling in Node.js applications:

typescript
// Global error handler for unhandled rejections
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason)

  // Attempt to identify content system errors
  if (reason instanceof ContentError) {
    // Log structured error information
    logStructuredError(reason)
  }
})

// Express error handling middleware
app.use((err, req, res, next) => {
  if (err instanceof ContentNotFoundError) {
    res.status(404).json({
      error: 'Not Found',
      message: err.message,
      path: err.uri,
    })
  } else if (err instanceof ContentAccessError) {
    res.status(403).json({
      error: 'Access Denied',
      message: err.message,
      path: err.uri,
    })
  } else if (err instanceof ContentValidationError) {
    res.status(400).json({
      error: 'Validation Error',
      message: err.message,
      validationErrors: err.info?.validationErrors || [],
    })
  } else {
    // Generic error
    res.status(500).json({
      error: 'Internal Server Error',
      message: 'An error occurred while processing your request',
    })
  }
})

Browser Error Handling

Error handling in browser applications:

typescript
// Global error handler
window.addEventListener('error', event => {
  // Check if it's a content system error
  if (event.error instanceof ContentError) {
    // Prevent default browser error handling
    event.preventDefault()

    // Log the error
    console.error('Content system error:', event.error)

    // Show user-friendly notification
    showErrorNotification(
      getUserFriendlyMessage(event.error),
      event.error.recoverable
    )
  }
})

// Helper to get user-friendly error messages
function getUserFriendlyMessage(error) {
  if (error instanceof ContentNotFoundError) {
    return `The content "${error.uri}" could not be found.`
  } else if (error instanceof ContentAccessError) {
    return `You don't have permission to access "${error.uri}".`
  } else if (error instanceof ContentValidationError) {
    return `The content has validation errors.`
  } else {
    return `An error occurred: ${error.message}`
  }
}

Error Logging and Monitoring

Structured Error Logging

Create structured logs for errors:

typescript
function logContentError(error) {
  if (!(error instanceof ContentError)) {
    return logGenericError(error)
  }

  const logEntry = {
    timestamp: new Date().toISOString(),
    errorType: error.name,
    message: error.message,
    operation: error.operation,
    uri: error.uri,
    source: error.source,
    recoverable: error.recoverable,
    info: error.info,
    stack: error.stack,
  }

  // Add cause if available
  if (error.cause) {
    logEntry.cause = {
      name: error.cause.name,
      message: error.cause.message,
      stack: error.cause.stack,
    }
  }

  // Log to service
  logger.error(logEntry)

  // Send to monitoring service in production
  if (process.env.NODE_ENV === 'production') {
    errorMonitoringService.captureError(error, {
      extra: logEntry,
    })
  }
}

Error Metrics

Collect metrics about errors:

typescript
class ErrorMetrics {
  private counts = {
    total: 0,
    byType: {} as Record<string, number>,
    byOperation: {} as Record<string, number>,
    recoverable: 0,
    nonRecoverable: 0,
  }

  recordError(error: Error) {
    // Increment total
    this.counts.total++

    if (error instanceof ContentError) {
      // Increment by type
      const type = error.name
      this.counts.byType[type] = (this.counts.byType[type] || 0) + 1

      // Increment by operation
      if (error.operation) {
        this.counts.byOperation[error.operation] =
          (this.counts.byOperation[error.operation] || 0) + 1
      }

      // Increment by recoverability
      if (error.recoverable) {
        this.counts.recoverable++
      } else {
        this.counts.nonRecoverable++
      }
    }

    // Send metrics to monitoring service
    if (this.counts.total % 10 === 0) {
      this.flushMetrics()
    }
  }

  flushMetrics() {
    metricsService.gauge('content.errors.total', this.counts.total)

    Object.entries(this.counts.byType).forEach(([type, count]) => {
      metricsService.gauge(`content.errors.byType.${type}`, count)
    })

    metricsService.gauge('content.errors.recoverable', this.counts.recoverable)
    metricsService.gauge(
      'content.errors.nonRecoverable',
      this.counts.nonRecoverable
    )
  }
}

// Create singleton instance
const errorMetrics = new ErrorMetrics()

// Register middleware
store.use(errorTrackingMiddleware())

function errorTrackingMiddleware() {
  return async (context, next) => {
    try {
      return await next()
    } catch (error) {
      // Record error metrics
      errorMetrics.recordError(error)

      // Rethrow
      throw error
    }
  }
}

Best Practices

  1. Type Checking: Always check error types for specific handling
  2. Proper Context: Include operation, URI, and source in errors
  3. User-Friendly Messages: Convert technical errors to user-friendly messages
  4. Recovery Options: Provide recovery paths for recoverable errors
  5. Centralized Handling: Use middleware for consistent error handling
  6. Environment Adaptation: Adapt error handling to the runtime environment
  7. Structured Logging: Log errors with structured metadata for analysis
  8. Fallback Content: Provide sensible defaults when content cannot be loaded
  9. Error Prevention: Use validation and checks to prevent errors
  10. Monitoring: Track error metrics for performance and reliability analysis

Released under the MIT License.