Skip to content

Error Handling

ts-pkgx provides several ways to handle errors when fetching and processing packages. This page covers advanced error handling techniques for more robust applications.

Custom Retry Logic

Implement custom retry logic for package fetching:

typescript
import { fetchPantryPackage } from 'ts-pkgx'

async function fetchWithRetry(packageName: string, maxRetries = 3): Promise<any> {
  let lastError

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      console.log(`Attempt ${attempt} of ${maxRetries} for ${packageName}`)
      return await fetchPantryPackage(packageName, {
        timeout: 30000 * attempt, // Increase timeout with each retry
      })
    }
    catch (error) {
      lastError = error
      console.error(`Attempt ${attempt} failed for ${packageName}:`, error.message)

      // Don't retry for certain errors
      if (error.message.includes('404') || error.message.includes('Not Found')) {
        throw new Error(`Package ${packageName} not found`)
      }

      // Wait before retrying
      if (attempt < maxRetries) {
        const delay = 2000 * attempt // Increasing delay
        console.log(`Waiting ${delay}ms before retry...`)
        await new Promise(resolve => setTimeout(resolve, delay))
      }
    }
  }

  throw new Error(`Failed to fetch ${packageName} after ${maxRetries} attempts: ${lastError.message}`)
}

Error Classification

Classify and handle different types of errors:

typescript
function classifyError(error: any, packageName: string): {
  type: 'not_found' | 'timeout' | 'parse_error' | 'network_error' | 'unknown'
  retryable: boolean
  message: string
} {
  const errorString = error.toString()

  if (errorString.includes('404') || errorString.includes('Not Found')) {
    return {
      type: 'not_found',
      retryable: false, // No point retrying a 404
      message: `Package ${packageName} not found`
    }
  }

  if (errorString.includes('Timeout') || errorString.includes('timed out')) {
    return {
      type: 'timeout',
      retryable: true, // Timeouts are often temporary
      message: `Timeout fetching ${packageName}`
    }
  }

  if (errorString.includes('JSON') || errorString.includes('parse')) {
    return {
      type: 'parse_error',
      retryable: true, // Parsing errors might be temporary
      message: `Error parsing data for ${packageName}`
    }
  }

  if (errorString.includes('network') || errorString.includes('connect') || errorString.includes('ECONNREFUSED')) {
    return {
      type: 'network_error',
      retryable: true, // Network errors are often temporary
      message: `Network error fetching ${packageName}`
    }
  }

  return {
    type: 'unknown',
    retryable: true, // Assume unknown errors are retryable
    message: `Unknown error fetching ${packageName}: ${errorString}`
  }
}

Fallback Content Creation

Create fallback content when package fetching fails:

typescript
import type { PkgxPackage } from 'ts-pkgx'
import fs from 'node:fs'
import path from 'node:path'

async function createFallbackPackage(
  packageName: string,
  outputDir: string,
  error: any
): Promise<PkgxPackage> {
  console.error(`Creating fallback package for ${packageName} due to error:`, error.message)

  // Extract domain and subpath if present
  const parts = packageName.split('/')
  const domain = parts[0]
  const subPath = parts.length > 1 ? parts.slice(1).join('/') : null

  // Create a minimal package with error info
  const fallbackPackage: PkgxPackage = {
    name: subPath || packageName,
    domain,
    description: `${packageName} package (Fallback due to fetch error)`,
    packageYmlUrl: `https://github.com/pkgxdev/pantry/tree/main/projects/${packageName}/package.yml`,
    homepageUrl: '',
    githubUrl: '',
    installCommand: `pkgx ${packageName}`,
    programs: [],
    companions: [],
    dependencies: [],
    versions: [],
    aliases: subPath ? [subPath] : undefined,
    fullPath: packageName,
    // Additional error information
    _error: {
      message: error.message,
      timestamp: new Date().toISOString(),
      classified: classifyError(error, packageName)
    }
  }

  // Save the fallback package
  const safeFilename = packageName.replace(/\./g, '').replace(/\//g, '-').toLowerCase()
  const filePath = path.join(outputDir, `${safeFilename}.json`)

  fs.writeFileSync(filePath, JSON.stringify(fallbackPackage, null, 2))
  console.log(`Created fallback package at ${filePath}`)

  return fallbackPackage
}

Error Logging

Implement comprehensive error logging:

typescript
import fs from 'node:fs'
import path from 'node:path'

interface ErrorLogEntry {
  timestamp: string
  packageName: string
  error: {
    message: string
    stack?: string
    type: string
  }
  context: Record<string, any>
}

class PackageErrorLogger {
  private logDir: string
  private logFile: string

  constructor(logDir = './logs') {
    this.logDir = logDir
    this.logFile = path.join(logDir, 'package-errors.log')

    // Ensure log directory exists
    if (!fs.existsSync(logDir)) {
      fs.mkdirSync(logDir, { recursive: true })
    }
  }

  log(packageName: string, error: any, context: Record<string, any> = {}): void {
    const entry: ErrorLogEntry = {
      timestamp: new Date().toISOString(),
      packageName,
      error: {
        message: error.message || String(error),
        stack: error.stack,
        type: error.constructor.name
      },
      context
    }

    // Append to log file
    fs.appendFileSync(
      this.logFile,
      `${JSON.stringify(entry)}\n`
    )

    // For significant errors, save a separate file
    if (this.isSignificantError(error)) {
      const filename = `${packageName.replace(/\W+/g, '-')}-${Date.now()}.error.json`
      fs.writeFileSync(
        path.join(this.logDir, filename),
        JSON.stringify(entry, null, 2)
      )
    }

    console.error(`Error logged for ${packageName}:`, error.message)
  }

  private isSignificantError(error: any): boolean {
    const errorString = String(error)

    // Consider these significant enough for individual logging
    return !(
      errorString.includes('timeout')
      || errorString.includes('429') // Too many requests
      || errorString.includes('404') // Not found
    )
  }

  getRecentErrors(count = 10): ErrorLogEntry[] {
    if (!fs.existsSync(this.logFile))
      return []

    const lines = fs.readFileSync(this.logFile, 'utf8')
      .split('\n')
      .filter(Boolean)
      .slice(-count)

    return lines.map(line => JSON.parse(line))
  }
}

// Usage
const errorLogger = new PackageErrorLogger()

try {
  // Some operation that might fail
  await fetchPantryPackage('example.com')
}
catch (error) {
  errorLogger.log('example.com', error, {
    attempt: 2,
    timestamp: Date.now()
  })
}

Graceful Degradation

Implement graceful degradation for batch operations:

typescript
async function batchFetchWithGracefulDegradation(
  packageNames: string[],
  options = {}
): Promise<{
    succeeded: Array<{ name: string, package: PkgxPackage }>
    failed: Array<{ name: string, error: any }>
  }> {
  const results = {
    succeeded: [],
    failed: []
  }

  // Process in batches to avoid overwhelming resources
  const BATCH_SIZE = 10
  const batches = Math.ceil(packageNames.length / BATCH_SIZE)

  for (let i = 0; i < batches; i++) {
    const start = i * BATCH_SIZE
    const end = Math.min(start + BATCH_SIZE, packageNames.length)
    const batch = packageNames.slice(start, end)

    console.log(`Processing batch ${i + 1}/${batches} (packages ${start + 1}-${end})`)

    // Process each package in the batch
    const batchResults = await Promise.allSettled(
      batch.map(async (packageName) => {
        try {
          const { packageInfo } = await fetchWithRetry(packageName)
          return { name: packageName, package: packageInfo }
        }
        catch (error) {
          // Log the error but don't fail the entire batch
          errorLogger.log(packageName, error)
          const packageError = new Error(`Failed to fetch package: ${packageName}`)
          packageError.cause = error
          packageError.packageName = packageName
          throw packageError
        }
      })
    )

    // Sort results
    batchResults.forEach((result) => {
      if (result.status === 'fulfilled') {
        results.succeeded.push(result.value)
      }
      else {
        results.failed.push(result.reason)
      }
    })

    // If too many failures, slow down or adjust batch size
    const failureRate = results.failed.length / (results.succeeded.length + results.failed.length)
    if (failureRate > 0.5 && batch.length > 1) {
      console.warn(`High failure rate (${failureRate.toFixed(2)}), reducing batch size and slowing down`)
      BATCH_SIZE = Math.max(1, Math.floor(BATCH_SIZE / 2))
      await new Promise(resolve => setTimeout(resolve, 5000)) // Cool-down period
    }
  }

  return results
}

Error Tracking and Analysis

Track and analyze errors to identify patterns:

typescript
class ErrorTracker {
  private errors: Map<string, {
    count: number
    lastSeen: Date
    examples: Array<{ error: any, context: any }>
  }> = new Map()

  track(errorType: string, error: any, context: any = {}): void {
    if (!this.errors.has(errorType)) {
      this.errors.set(errorType, {
        count: 0,
        lastSeen: new Date(),
        examples: []
      })
    }

    const entry = this.errors.get(errorType)!
    entry.count++
    entry.lastSeen = new Date()

    // Keep some examples but limit to avoid memory issues
    if (entry.examples.length < 5) {
      entry.examples.push({ error, context })
    }
  }

  getReport(): Record<string, any> {
    const report: Record<string, any> = {}

    this.errors.forEach((data, errorType) => {
      report[errorType] = {
        count: data.count,
        lastSeen: data.lastSeen.toISOString(),
        examples: data.examples.map(e => ({
          message: e.error.message || String(e.error),
          context: e.context
        }))
      }
    })

    return report
  }

  resetCounts(): void {
    this.errors.forEach((data) => {
      data.count = 0
    })
  }
}

// Usage
const errorTracker = new ErrorTracker()

// When an error occurs
try {
  await fetchPantryPackage('example.com')
}
catch (error) {
  const errorType = classifyError(error, 'example.com').type
  errorTracker.track(errorType, error, {
    packageName: 'example.com',
    timestamp: Date.now()
  })
}

// Generate a report
console.log(errorTracker.getReport())

These advanced error handling techniques will help you build more robust applications that can gracefully handle failures when working with ts-pkgx.

Released under the MIT License.