Skip to content

Error Handling Examples

This document provides practical examples of error handling in the content system. It includes code snippets for common error scenarios and their recommended handling approaches.

Basic Error Handling Examples

Handling Content Not Found

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

async function loadArticle(uri) {
  try {
    return await store.read(uri)
  } catch (error) {
    if (error instanceof ContentNotFoundError) {
      console.log(`Article not found: ${uri}`)

      // Return a default "not found" article
      return {
        data: `# Article Not Found\n\nSorry, the article at ${uri} could not be found.`,
        contentType: 'text/markdown',
        metadata: {
          title: 'Article Not Found',
          status: 'error',
        },
      }
    }

    // Re-throw other errors
    throw error
  }
}

// Usage
const article = await loadArticle('articles/missing-article.md')
displayArticle(article) // Displays "Article Not Found" content

Handling Content Validation Errors

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

async function saveArticle(uri, content) {
  try {
    await store.write(uri, content)
    showSuccessMessage('Article saved successfully')
    return true
  } catch (error) {
    if (error instanceof ContentValidationError) {
      // Extract validation errors from the error
      const validationErrors = error.info?.validationErrors || []

      // Display validation errors to the user
      showValidationErrors(validationErrors)

      // Return false to indicate save failed
      return false
    }

    // Handle other errors
    showErrorMessage(`Failed to save article: ${error.message}`)
    return false
  }
}

// Usage
const saved = await saveArticle('articles/new.md', invalidContent)
if (!saved) {
  // Keep editor open for user to fix errors
  keepEditorOpen()
}

Handling Access Errors

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

async function editArticle(uri) {
  try {
    const content = await store.read(uri)
    openEditor(uri, content)
  } catch (error) {
    if (error instanceof ContentAccessError) {
      // Prompt user to authenticate
      showLoginPrompt('You need to log in to edit this article', () => {
        // Retry after login
        editArticle(uri)
      })
    } else {
      // Handle other errors
      showErrorMessage(`Could not open article: ${error.message}`)
    }
  }
}

// Usage
editArticle('articles/protected.md')

Handling Operation Errors

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

async function batchProcess(uris) {
  const results = {
    succeeded: [],
    failed: [],
  }

  for (const uri of uris) {
    try {
      // Perform operation
      const content = await store.read(uri)
      const processed = processContent(content)
      await store.write(uri, processed)

      // Record success
      results.succeeded.push(uri)
    } catch (error) {
      if (error instanceof ContentOperationError) {
        console.error(
          `Operation ${error.operation} failed on ${uri}: ${error.message}`
        )
      } else {
        console.error(`Unknown error processing ${uri}:`, error)
      }

      // Record failure
      results.failed.push({
        uri,
        error: error.message,
        type: error.name,
      })
    }
  }

  return results
}

// Usage
const results = await batchProcess([
  'articles/article1.md',
  'articles/article2.md',
  'articles/article3.md',
])

showProcessingResults(results)

Advanced Error Handling Examples

Using Error Recovery Functions

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

async function loadContentWithRecovery(uri) {
  try {
    return await store.read(uri)
  } catch (error) {
    if (error instanceof ContentError && error.recoverable && error.recovery) {
      try {
        // Attempt to recover using the provided recovery function
        console.log(`Attempting to recover content for ${uri}`)
        const recoveredContent = await error.recovery()

        // Mark content as recovered
        if (recoveredContent) {
          recoveredContent.metadata = {
            ...recoveredContent.metadata,
            recovered: true,
            recoveryTimestamp: new Date(),
          }
        }

        return recoveredContent
      } catch (recoveryError) {
        console.error(`Recovery failed for ${uri}:`, recoveryError)
        // Fall through to default error handling
      }
    }

    // Handle unrecoverable errors
    console.error(`Unrecoverable error for ${uri}:`, error)
    throw error
  }
}

// Usage
try {
  const content = await loadContentWithRecovery('articles/corrupted.md')
  displayContent(content)

  // Show recovery notice if content was recovered
  if (content.metadata.recovered) {
    showRecoveryNotice()
  }
} catch (error) {
  showErrorMessage('Could not load content, even with recovery attempts')
}

Implementing Custom Error Classes

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

// Create a custom error class for specific scenarios
class ArticlePublishingError extends ContentError {
  constructor(uri, reason, options = {}) {
    super(`Failed to publish article at ${uri}: ${reason}`, {
      uri,
      operation: 'publish',
      source: 'publishing-service',
      ...options,
    })

    this.name = 'ArticlePublishingError'
    this.reason = reason
  }
}

// Publishing service with custom error
async function publishArticle(uri) {
  try {
    // Get article content
    const article = await store.read(uri)

    // Check if article is ready for publishing
    if (!article.metadata.readyToPublish) {
      throw new ArticlePublishingError(uri, 'Article is not marked as ready', {
        recoverable: true,
        recovery: async () => {
          // Auto-fix by setting readyToPublish
          article.metadata.readyToPublish = true
          await store.write(uri, article)
          return publishArticle(uri)
        },
      })
    }

    // Publish to external service
    await publishToExternalService(article)

    // Update status
    article.metadata.status = 'published'
    article.metadata.publishedAt = new Date()
    await store.write(uri, article)

    return {
      success: true,
      publishedAt: article.metadata.publishedAt,
    }
  } catch (error) {
    if (error instanceof ArticlePublishingError) {
      console.error(`Publishing error: ${error.message}`)

      if (error.recoverable && error.recovery) {
        if (await confirmRecovery(error.reason)) {
          return error.recovery()
        }
      }
    }

    // Re-throw other errors
    throw error
  }
}

// Usage
try {
  const result = await publishArticle('articles/draft.md')
  showSuccessMessage(`Article published at ${result.publishedAt}`)
} catch (error) {
  showErrorMessage(`Publishing failed: ${error.message}`)
}

Using Error Middleware

typescript
import { createMiddleware } from '@lib/content/middleware'
import {
  ContentNotFoundError,
  ContentValidationError,
} from '@lib/content/errors'

// Create custom error handling middleware
const errorHandlingMiddleware = createMiddleware((options = {}) => {
  return async (context, next) => {
    try {
      // Attempt the normal operation
      return await next()
    } catch (error) {
      // Log all errors
      console.error(`Error in ${context.operation} on ${context.uri}:`, error)

      // Handle specific error types
      if (error instanceof ContentNotFoundError && options.provideFallbacks) {
        // Provide fallback content
        console.log(`Providing fallback for ${context.uri}`)
        context.content = createFallbackContent(context.uri)
        return context
      }

      if (error instanceof ContentValidationError && options.autoFix) {
        // Attempt to auto-fix validation errors
        if (context.operation === 'write' && context.content) {
          console.log(`Attempting to auto-fix ${context.uri}`)
          context.content = autofixContent(
            context.content,
            error.info?.validationErrors
          )

          // Try operation again with fixed content
          return await next()
        }
      }

      // Rethrow the error for other error types
      throw error
    }
  }
})

// Create store with error handling middleware
const storeWithErrorHandling = createContentStore({
  adapter: createMemoryAdapter(),
  middleware: [
    errorHandlingMiddleware({
      provideFallbacks: true,
      autoFix: true,
    }),
  ],
})

// Helper functions
function createFallbackContent(uri) {
  return {
    data: `# Fallback Content\n\nOriginal content at ${uri} was not found.`,
    contentType: 'text/markdown',
    metadata: {
      title: 'Fallback Content',
      isFallback: true,
    },
  }
}

function autofixContent(content, validationErrors = []) {
  // Clone content to avoid mutation
  const fixedContent = {
    ...content,
    metadata: { ...content.metadata },
  }

  // Apply fixes based on validation errors
  for (const error of validationErrors) {
    if (error.path === 'metadata.title' && !fixedContent.metadata.title) {
      fixedContent.metadata.title = 'Untitled'
    }

    if (
      error.path === 'metadata.createdAt' &&
      !fixedContent.metadata.createdAt
    ) {
      fixedContent.metadata.createdAt = new Date()
    }

    // Add other auto-fixes as needed
  }

  return fixedContent
}

// Usage
async function loadContentSafely(uri) {
  // This will automatically use fallbacks for not found errors
  const content = await storeWithErrorHandling.read(uri)

  // Check if fallback was used
  if (content.metadata.isFallback) {
    showFallbackNotice()
  }

  return content
}

React Integration Examples

React Error Boundary

tsx
import React from 'react'
import {
  ContentError,
  ContentNotFoundError,
  ContentAccessError,
} from '@lib/content/errors'

// Generic error boundary for content components
class ContentErrorBoundary extends React.Component {
  state = {
    hasError: false,
    error: null,
  }

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

  componentDidCatch(error, errorInfo) {
    // Log error to service
    console.error('Content error caught:', error)
    logErrorToService(error, errorInfo)
  }

  render() {
    if (!this.state.hasError) {
      return this.props.children
    }

    const error = this.state.error

    // Different UI based on error type
    if (error instanceof ContentNotFoundError) {
      return (
        <div className="error-container not-found">
          <h2>Content Not Found</h2>
          <p>The content at {error.uri} could not be found.</p>
          <button onClick={this.props.onRetry}>Retry</button>
        </div>
      )
    }

    if (error instanceof ContentAccessError) {
      return (
        <div className="error-container access-denied">
          <h2>Access Denied</h2>
          <p>You don't have permission to access {error.uri}.</p>
          <button onClick={this.props.onLogin}>Log In</button>
        </div>
      )
    }

    // Generic error handling for other error types
    return (
      <div className="error-container">
        <h2>Error Loading Content</h2>
        <p>{error.message}</p>
        <button onClick={this.props.onRetry}>Retry</button>
      </div>
    )
  }
}

// Usage in content component
function ArticleViewer({ uri }) {
  const [isLoading, setIsLoading] = useState(true)
  const [article, setArticle] = useState(null)

  const loadArticle = useCallback(async () => {
    try {
      setIsLoading(true)
      const content = await store.read(uri)
      setArticle(content)
      setIsLoading(false)
    } catch (error) {
      console.error('Error loading article:', error)
      // Let the error boundary handle the error
      throw error
    }
  }, [uri])

  useEffect(() => {
    loadArticle()
  }, [loadArticle])

  if (isLoading) {
    return <LoadingSpinner />
  }

  return (
    <article>
      <h1>{article.metadata.title}</h1>
      <div className="content">{renderMarkdown(article.data)}</div>
    </article>
  )
}

// Wrapped with error boundary
function SafeArticleViewer({ uri }) {
  const handleRetry = () => {
    // Force re-mount of the component
    setKey(prev => prev + 1)
  }

  const handleLogin = () => {
    // Show login modal
    showLoginModal(() => handleRetry())
  }

  const [key, setKey] = useState(0)

  return (
    <ContentErrorBoundary key={key} onRetry={handleRetry} onLogin={handleLogin}>
      <ArticleViewer uri={uri} />
    </ContentErrorBoundary>
  )
}

React Hooks for Error Handling

tsx
import { useState, useEffect, useCallback } from 'react'
import { ContentNotFoundError, ContentAccessError } from '@lib/content/errors'

// Custom hook for content loading with error handling
function useContentWithErrorHandling(uri) {
  const [state, setState] = useState({
    isLoading: true,
    content: null,
    error: null,
    errorType: null,
    isNotFound: false,
    isAccessDenied: false,
  })

  const loadContent = useCallback(async () => {
    try {
      setState(prev => ({ ...prev, isLoading: true, error: null }))

      const content = await store.read(uri)

      setState({
        isLoading: false,
        content,
        error: null,
        errorType: null,
        isNotFound: false,
        isAccessDenied: false,
      })
    } catch (error) {
      // Categorize error
      const isNotFound = error instanceof ContentNotFoundError
      const isAccessDenied = error instanceof ContentAccessError
      const errorType = error.name

      setState({
        isLoading: false,
        content: null,
        error,
        errorType,
        isNotFound,
        isAccessDenied,
      })

      // Log error
      console.error(`Error loading ${uri}:`, error)
    }
  }, [uri])

  useEffect(() => {
    loadContent()
  }, [loadContent])

  return {
    ...state,
    reload: loadContent,
  }
}

// Usage in a component
function ContentDisplay({ uri }) {
  const { isLoading, content, error, isNotFound, isAccessDenied, reload } =
    useContentWithErrorHandling(uri)

  if (isLoading) {
    return <LoadingSpinner />
  }

  if (isNotFound) {
    return (
      <div className="error not-found">
        <h2>Content Not Found</h2>
        <p>The requested content could not be found.</p>
        <button onClick={reload}>Retry</button>
      </div>
    )
  }

  if (isAccessDenied) {
    return (
      <div className="error access-denied">
        <h2>Access Denied</h2>
        <p>You don't have permission to access this content.</p>
        <button onClick={showLoginModal}>Log In</button>
      </div>
    )
  }

  if (error) {
    return (
      <div className="error">
        <h2>Error</h2>
        <p>{error.message}</p>
        <button onClick={reload}>Retry</button>
      </div>
    )
  }

  return (
    <div className="content">
      <h1>{content.metadata.title}</h1>
      <div>{renderContent(content)}</div>
    </div>
  )
}

Node.js Server Examples

Express API Error Handling

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

const app = express()

// Content API routes
app.get('/api/content/:path(*)', async (req, res, next) => {
  try {
    const uri = req.params.path
    const content = await store.read(uri)

    res.json({
      uri,
      data: content.data,
      metadata: content.metadata,
      contentType: content.contentType,
    })
  } catch (error) {
    // Pass the error to the error handler middleware
    next(error)
  }
})

app.put('/api/content/:path(*)', async (req, res, next) => {
  try {
    const uri = req.params.path
    const { data, contentType, metadata } = req.body

    await store.write(uri, { data, contentType, metadata })

    res.json({
      success: true,
      uri,
    })
  } catch (error) {
    next(error)
  }
})

app.delete('/api/content/:path(*)', async (req, res, next) => {
  try {
    const uri = req.params.path
    await store.delete(uri)

    res.json({
      success: true,
      uri,
    })
  } catch (error) {
    next(error)
  }
})

// Error handling middleware
app.use((error, req, res, next) => {
  if (error instanceof ContentNotFoundError) {
    return res.status(404).json({
      error: 'NOT_FOUND',
      message: `Content not found: ${error.uri}`,
      uri: error.uri,
    })
  }

  if (error instanceof ContentAccessError) {
    return res.status(403).json({
      error: 'ACCESS_DENIED',
      message: `Access denied: ${error.uri}`,
      uri: error.uri,
    })
  }

  if (error instanceof ContentValidationError) {
    return res.status(400).json({
      error: 'VALIDATION_ERROR',
      message: `Validation failed: ${error.message}`,
      validationErrors: error.info?.validationErrors || [],
      uri: error.uri,
    })
  }

  // Log unknown errors
  console.error('Unhandled error in API:', error)

  // Generic error response
  res.status(500).json({
    error: 'INTERNAL_ERROR',
    message: 'An internal server error occurred',
  })
})

// Start the server
app.listen(3000, () => {
  console.log('Content API server running on port 3000')
})

HTTP Client with Error Handling

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

// HTTP client for content API
class ContentApiClient {
  constructor(baseUrl) {
    this.baseUrl = baseUrl
  }

  async read(uri) {
    try {
      const response = await fetch(`${this.baseUrl}/api/content/${uri}`)

      if (!response.ok) {
        await this.handleErrorResponse(response, 'read', uri)
      }

      const result = await response.json()

      return {
        data: result.data,
        contentType: result.contentType,
        metadata: result.metadata,
      }
    } catch (error) {
      if (error instanceof ContentError) {
        throw error
      }

      // Convert network/fetch errors to ContentErrors
      throw new ContentError(`Network error reading ${uri}: ${error.message}`, {
        uri,
        operation: 'read',
        source: 'api-client',
        cause: error,
      })
    }
  }

  async write(uri, content) {
    try {
      const response = await fetch(`${this.baseUrl}/api/content/${uri}`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(content),
      })

      if (!response.ok) {
        await this.handleErrorResponse(response, 'write', uri)
      }

      return await response.json()
    } catch (error) {
      if (error instanceof ContentError) {
        throw error
      }

      throw new ContentError(`Network error writing ${uri}: ${error.message}`, {
        uri,
        operation: 'write',
        source: 'api-client',
        cause: error,
      })
    }
  }

  async delete(uri) {
    try {
      const response = await fetch(`${this.baseUrl}/api/content/${uri}`, {
        method: 'DELETE',
      })

      if (!response.ok) {
        await this.handleErrorResponse(response, 'delete', uri)
      }

      return await response.json()
    } catch (error) {
      if (error instanceof ContentError) {
        throw error
      }

      throw new ContentError(
        `Network error deleting ${uri}: ${error.message}`,
        {
          uri,
          operation: 'delete',
          source: 'api-client',
          cause: error,
        }
      )
    }
  }

  async handleErrorResponse(response, operation, uri) {
    let errorData

    try {
      errorData = await response.json()
    } catch (e) {
      errorData = {
        error: 'UNKNOWN_ERROR',
        message: `Unknown error: ${response.statusText}`,
      }
    }

    switch (response.status) {
      case 404:
        throw new ContentNotFoundError(
          errorData.message || `Content not found: ${uri}`,
          {
            uri,
            operation,
            source: 'api-client',
          }
        )

      case 403:
        throw new ContentAccessError(
          errorData.message || `Access denied: ${uri}`,
          {
            uri,
            operation,
            source: 'api-client',
          }
        )

      case 400:
        throw new ContentValidationError(
          errorData.message || `Validation error: ${uri}`,
          {
            uri,
            operation,
            source: 'api-client',
            info: {
              validationErrors: errorData.validationErrors || [],
            },
          }
        )

      default:
        throw new ContentError(
          errorData.message || `API error: ${response.statusText}`,
          {
            uri,
            operation,
            source: 'api-client',
            info: errorData,
          }
        )
    }
  }
}

// Usage
const apiClient = new ContentApiClient('https://api.example.com')

try {
  const content = await apiClient.read('articles/welcome.md')
  console.log('Content loaded:', content.metadata.title)
} catch (error) {
  if (error instanceof ContentNotFoundError) {
    console.log('Article not found, creating a new one')
    await apiClient.write('articles/welcome.md', {
      data: '# Welcome\n\nThis is a new article.',
      contentType: 'text/markdown',
      metadata: { title: 'Welcome' },
    })
  } else if (error instanceof ContentAccessError) {
    console.log('Authentication required')
    await authenticateUser()
    // Retry after authentication
  } else {
    console.error('Error accessing content:', error)
  }
}

Released under the MIT License.