Skip to content

Error Types

This page documents the error type system in ReX which provides a consistent approach to error handling and recovery.

Core Error Interface

typescript
/**
 * Base interface for error recovery options
 */
interface ContentErrorRecoveryOptions {
  /**
   * Whether the error can be retried
   */
  canRetry?: boolean

  /**
   * Suggested retry delay in milliseconds
   */
  retryDelay?: number

  /**
   * Maximum number of retries suggested
   */
  maxRetries?: number

  /**
   * Alternative URIs that may contain the content
   */
  alternativeUris?: string[]

  /**
   * Fallback content that can be used
   */
  fallbackContent?: Content<any>

  /**
   * Additional context for recovery
   */
  [key: string]: any
}

/**
 * Base error class for content-related errors
 */
class ContentError extends Error {
  /**
   * Create a new ContentError
   * @param message Error message
   * @param uri Content URI related to the error
   * @param source Source of the error (adapter name, etc.)
   * @param recoverable Whether the error is recoverable
   * @param recoveryOptions Options for recovery
   */
  constructor(
    message: string,
    public readonly uri?: string,
    public readonly source?: string,
    public readonly recoverable: boolean = false,
    public readonly recoveryOptions?: ContentErrorRecoveryOptions
  ) {
    super(message)
    this.name = 'ContentError'
  }

  /**
   * Get a string representation of the error
   */
  toString(): string {
    return `${this.name}: ${this.message}${this.uri ? ` (uri: ${this.uri})` : ''}${this.source ? ` (source: ${this.source})` : ''}`
  }
}

Specialized Error Types

The system defines specific error types for different scenarios:

Not Found Errors

typescript
/**
 * Error thrown when content is not found
 */
class ContentNotFoundError extends ContentError {
  constructor(
    uri: string,
    source?: string,
    recoveryOptions?: ContentErrorRecoveryOptions
  ) {
    super(
      `Content not found at URI: ${uri}`,
      uri,
      source,
      false,
      recoveryOptions
    )
    this.name = 'ContentNotFoundError'
  }
}

Access Errors

typescript
/**
 * Error thrown when content cannot be accessed due to permissions
 */
class ContentAccessError extends ContentError {
  constructor(
    uri: string,
    source?: string,
    recoveryOptions?: ContentErrorRecoveryOptions
  ) {
    super(
      `Access denied for content at URI: ${uri}`,
      uri,
      source,
      false,
      recoveryOptions
    )
    this.name = 'ContentAccessError'
  }
}

Format Errors

typescript
/**
 * Error thrown when content format is invalid
 */
class ContentFormatError extends ContentError {
  constructor(
    uri: string,
    details?: string,
    source?: string,
    recoveryOptions?: ContentErrorRecoveryOptions
  ) {
    super(
      `Invalid content format at URI: ${uri}${details ? ` (${details})` : ''}`,
      uri,
      source,
      true,
      {
        canRetry: false,
        ...recoveryOptions,
      }
    )
    this.name = 'ContentFormatError'
  }
}

Validation Errors

typescript
/**
 * Error thrown when content validation fails
 */
class ContentValidationError extends ContentError {
  /**
   * Create a new ContentValidationError
   * @param uri Content URI
   * @param validationErrors Array of validation error messages
   * @param source Error source
   * @param recoveryOptions Recovery options
   */
  constructor(
    uri: string,
    public readonly validationErrors: string[],
    source?: string,
    recoveryOptions?: ContentErrorRecoveryOptions
  ) {
    super(
      `Content validation failed at URI: ${uri}: ${validationErrors.join(', ')}`,
      uri,
      source,
      true,
      {
        canRetry: true,
        validationErrors,
        ...recoveryOptions,
      }
    )
    this.name = 'ContentValidationError'
  }
}

Operation Errors

typescript
/**
 * Error thrown when a content operation fails
 */
class ContentOperationError extends ContentError {
  /**
   * Create a new ContentOperationError
   * @param operation Operation name
   * @param uri Content URI
   * @param cause Original error
   * @param source Error source
   * @param recoveryOptions Recovery options
   */
  constructor(
    public readonly operation: string,
    uri: string,
    public readonly cause?: Error,
    source?: string,
    recoveryOptions?: ContentErrorRecoveryOptions
  ) {
    super(
      `Content operation '${operation}' failed for URI: ${uri}${cause ? ` (${cause.message})` : ''}`,
      uri,
      source,
      !!recoveryOptions?.canRetry,
      recoveryOptions
    )
    this.name = 'ContentOperationError'
  }
}

Timeout Errors

typescript
/**
 * Error thrown when a content operation times out
 */
class ContentTimeoutError extends ContentError {
  /**
   * Create a new ContentTimeoutError
   * @param operation Operation name
   * @param uri Content URI
   * @param timeoutMs Timeout in milliseconds
   * @param source Error source
   * @param recoveryOptions Recovery options
   */
  constructor(
    public readonly operation: string,
    uri: string,
    public readonly timeoutMs: number,
    source?: string,
    recoveryOptions?: ContentErrorRecoveryOptions
  ) {
    super(
      `Content operation '${operation}' timed out after ${timeoutMs}ms for URI: ${uri}`,
      uri,
      source,
      true,
      {
        canRetry: true,
        retryDelay: Math.min(timeoutMs * 2, 30000), // Double the timeout, max 30s
        ...recoveryOptions,
      }
    )
    this.name = 'ContentTimeoutError'
  }
}

Conflict Errors

typescript
/**
 * Error thrown when a content conflict occurs
 */
class ContentConflictError extends ContentError {
  /**
   * Create a new ContentConflictError
   * @param uri Content URI
   * @param source Error source
   * @param conflictingContent Conflicting content
   * @param recoveryOptions Recovery options
   */
  constructor(
    uri: string,
    source?: string,
    public readonly conflictingContent?: Content<any>,
    recoveryOptions?: ContentErrorRecoveryOptions
  ) {
    super(`Content conflict at URI: ${uri}`, uri, source, true, {
      canRetry: false,
      conflictingContent,
      ...recoveryOptions,
    })
    this.name = 'ContentConflictError'
  }
}

Capability Errors

typescript
/**
 * Error thrown when an adapter doesn't support an operation
 */
class ContentCapabilityError extends ContentError {
  /**
   * Create a new ContentCapabilityError
   * @param operation Unsupported operation
   * @param adapter Adapter name
   * @param recoveryOptions Recovery options
   */
  constructor(
    public readonly operation: string,
    public readonly adapter: string,
    recoveryOptions?: ContentErrorRecoveryOptions
  ) {
    super(
      `Operation '${operation}' not supported by adapter '${adapter}'`,
      undefined,
      adapter,
      false,
      recoveryOptions
    )
    this.name = 'ContentCapabilityError'
  }
}

Transformation Errors

typescript
/**
 * Error thrown when content transformation fails
 */
class ContentTransformationError extends ContentError {
  /**
   * Create a new ContentTransformationError
   * @param uri Content URI
   * @param sourceType Source content type
   * @param targetType Target content type
   * @param cause Original error
   * @param source Error source
   * @param recoveryOptions Recovery options
   */
  constructor(
    uri: string,
    public readonly sourceType: string,
    public readonly targetType: string,
    public readonly cause?: Error,
    source?: string,
    recoveryOptions?: ContentErrorRecoveryOptions
  ) {
    super(
      `Failed to transform content from '${sourceType}' to '${targetType}' at URI: ${uri}${cause ? ` (${cause.message})` : ''}`,
      uri,
      source,
      false,
      recoveryOptions
    )
    this.name = 'ContentTransformationError'
  }
}

Error Utilities

The system provides utilities for working with errors:

typescript
/**
 * Check if an error is a ContentError
 * @param error Error to check
 */
function isContentError(error: Error): error is ContentError {
  return error instanceof ContentError || error.name === 'ContentError'
}

/**
 * Check if an error is a specific type of ContentError
 * @param error Error to check
 * @param errorType Error class or name
 */
function isErrorOfType(
  error: Error,
  errorType: typeof ContentError | string
): boolean {
  if (typeof errorType === 'string') {
    return error.name === errorType
  }
  return error instanceof errorType
}

/**
 * Create a ContentError from any error
 * @param error Original error
 * @param uri Content URI
 * @param source Error source
 */
function wrapError(error: Error, uri?: string, source?: string): ContentError {
  if (isContentError(error)) {
    return error
  }

  return new ContentOperationError('unknown', uri || 'unknown', error, source, {
    canRetry: false,
  })
}

Usage Examples

Basic Error Handling

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

// Create a store
const store = createContentStore()

// Read with error handling
async function readContent(uri: string) {
  try {
    const content = await store.read(uri)
    return content
  } catch (error) {
    if (error instanceof ContentNotFoundError) {
      console.error(`Content not found: ${error.uri}`)
      return null
    } else if (error instanceof ContentAccessError) {
      console.error(`Access denied: ${error.uri}`)
      await requestPermission(error.uri)
      return null
    } else {
      console.error(`Unexpected error: ${error.message}`)
      throw error
    }
  }
}

Recovery Options

typescript
import { createContentStore } from '@lib/content'
import { isContentError } from '@lib/content/errors'

// Create a store
const store = createContentStore()

// Read with recovery
async function readWithRecovery(uri: string, maxRetries = 3) {
  let retries = 0

  while (true) {
    try {
      return await store.read(uri)
    } catch (error) {
      if (
        isContentError(error) &&
        error.recoverable &&
        error.recoveryOptions?.canRetry
      ) {
        if (retries >= maxRetries) {
          console.error(`Max retries (${maxRetries}) exceeded for ${uri}`)
          throw error
        }

        const delay = error.recoveryOptions.retryDelay || 1000
        console.warn(
          `Retrying after ${delay}ms (${retries + 1}/${maxRetries})...`
        )

        await new Promise(resolve => setTimeout(resolve, delay))
        retries++
        continue
      }

      // Try alternative URIs if available
      if (
        isContentError(error) &&
        error.recoveryOptions?.alternativeUris?.length
      ) {
        for (const altUri of error.recoveryOptions.alternativeUris) {
          try {
            console.log(`Trying alternative URI: ${altUri}`)
            return await store.read(altUri)
          } catch (altError) {
            console.warn(`Alternative URI failed: ${altError.message}`)
          }
        }
      }

      // Use fallback content if available
      if (isContentError(error) && error.recoveryOptions?.fallbackContent) {
        console.warn(`Using fallback content for ${uri}`)
        return error.recoveryOptions.fallbackContent
      }

      throw error
    }
  }
}

Custom Error Classes

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

/**
 * Error thrown when rate limiting occurs
 */
class RateLimitError extends ContentError {
  /**
   * Create a new RateLimitError
   * @param uri Content URI
   * @param retryAfter Retry after seconds
   * @param source Error source
   */
  constructor(
    uri: string,
    public readonly retryAfter: number,
    source?: string
  ) {
    super(
      `Rate limit exceeded for URI: ${uri}. Retry after ${retryAfter} seconds.`,
      uri,
      source,
      true,
      {
        canRetry: true,
        retryDelay: retryAfter * 1000,
      }
    )
    this.name = 'RateLimitError'
  }
}

// Example HTTP adapter with rate limit handling
const httpAdapter = createAdapter({
  name: 'http',
  read: async uri => {
    try {
      const response = await fetch(`https://api.example.com/${uri}`)

      if (response.status === 429) {
        const retryAfter = parseInt(
          response.headers.get('Retry-After') || '60',
          10
        )
        throw new RateLimitError(uri, retryAfter, 'http')
      }

      if (!response.ok) {
        throw new Error(`HTTP error ${response.status}: ${response.statusText}`)
      }

      const data = await response.text()
      return {
        data,
        contentType: response.headers.get('Content-Type') || 'text/plain',
        metadata: {
          title: uri.split('/').pop() || uri,
          updatedAt: new Date(),
        },
      }
    } catch (error) {
      if (error instanceof RateLimitError) {
        throw error
      }

      throw new ContentOperationError('read', uri, error, 'http')
    }
  },
})

Middleware Error Handling

typescript
import { pipe, withErrorHandling } from '@lib/content/middleware'
import { isContentError, ContentNotFoundError } from '@lib/content/errors'

// Create a store with error handling middleware
const store = pipe(
  baseStore,
  withErrorHandling({
    onError: (error, operation, uri) => {
      console.error(
        `Error in ${operation} operation for ${uri}: ${error.message}`
      )

      // Report errors to monitoring system
      reportError(error, { operation, uri })

      // Don't transform the error, let it propagate
      return error
    },

    transformError: (error, operation, uri) => {
      // Convert 404 errors to a custom format
      if (error instanceof ContentNotFoundError) {
        return new CustomNotFoundError(uri, error.source)
      }

      // Add context to errors
      if (isContentError(error)) {
        error.context = {
          ...error.context,
          timestamp: new Date(),
          operation,
          uri,
        }
      }

      return error
    },
  })
)

Error Patterns

The error system is designed to support several important patterns:

1. Error Classification

Errors are classified by type, enabling specific handling strategies:

typescript
try {
  await store.read('doc.md')
} catch (error) {
  if (error instanceof ContentNotFoundError) {
    // Handle not found
  } else if (error instanceof ContentAccessError) {
    // Handle access denied
  } else if (error instanceof ContentFormatError) {
    // Handle invalid format
  } else {
    // Handle other errors
  }
}

2. Error Recovery

Errors include recovery information to guide recovery strategies:

typescript
try {
  await store.read('doc.md')
} catch (error) {
  if (isContentError(error) && error.recoverable) {
    const options = error.recoveryOptions || {}

    if (options.canRetry) {
      // Implement retry logic
    }

    if (options.alternativeUris?.length) {
      // Try alternative URIs
    }

    if (options.fallbackContent) {
      // Use fallback content
    }
  }
}

3. Error Enrichment

Errors are enriched with context for better debugging:

typescript
try {
  await store.write('doc.md', content)
} catch (error) {
  if (isContentError(error)) {
    console.error(`
      Error: ${error.message}
      URI: ${error.uri}
      Source: ${error.source}
      Operation: ${error instanceof ContentOperationError ? error.operation : 'unknown'}
      Recoverable: ${error.recoverable ? 'Yes' : 'No'}
    `)
  }
}

4. Error Translation

Errors from underlying systems are translated to ContentErrors:

typescript
async function readFile(uri) {
  try {
    // Convert URI to filesystem path
    const path = convertUriToPath(uri)
    return await fs.promises.readFile(path, 'utf8')
  } catch (error) {
    if (error.code === 'ENOENT') {
      throw new ContentNotFoundError(uri, 'filesystem')
    } else if (error.code === 'EACCES') {
      throw new ContentAccessError(uri, 'filesystem')
    } else {
      throw new ContentOperationError('read', uri, error, 'filesystem')
    }
  }
}

Released under the MIT License.