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.