> ## Documentation Index
> Fetch the complete documentation index at: https://docs.browsertest.in/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Handling

> Comprehensive error handling strategies for BrowserTest SDK

## Overview

BrowserTest SDK provides specific error types and comprehensive error handling mechanisms. Understanding and properly handling errors is crucial for building robust applications.

## Error Types

### BrowserTestAuthError

Thrown when API key authentication fails.

```javascript theme={null}
try {
  await bt.screenshot.take({ url: 'https://example.com' });
} catch (error) {
  if (error.name === 'BrowserTestAuthError') {
    console.log('Authentication failed - check your API key');
    // Handle auth error: prompt user to check API key, retry with new key, etc.
  }
}
```

### BrowserTestQuotaError

Thrown when API quota limits are exceeded.

```javascript theme={null}
try {
  await bt.testing.execute({ instructions: 'test', url: 'https://example.com' });
} catch (error) {
  if (error.name === 'BrowserTestQuotaError') {
    console.log('Quota exceeded:', error.quotaInfo);
    console.log('Used:', error.quotaInfo.usage.screenshot.used);
    console.log('Limit:', error.quotaInfo.usage.screenshot.limit);
    // Handle quota exceeded: notify user, suggest upgrading plan, etc.
  }
}
```

### BrowserTestAPIError

Thrown for general API errors (server errors, invalid requests, etc.).

```javascript theme={null}
try {
  await bt.screenshot.take({ url: 'invalid-url' });
} catch (error) {
  if (error.name === 'BrowserTestAPIError') {
    console.log('API error:', error.status, error.message);
    // Handle based on status code
    switch (error.status) {
      case 400:
        console.log('Bad request - check your parameters');
        break;
      case 404:
        console.log('Resource not found');
        break;
      case 500:
        console.log('Server error - retry later');
        break;
    }
  }
}
```

### BrowserTestTimeoutError

Thrown when requests exceed the configured timeout.

```javascript theme={null}
try {
  await bt.testing.execute({
    instructions: 'complex test',
    url: 'https://slow-site.com',
    config: { timeout: 10000 }
  });
} catch (error) {
  if (error.name === 'BrowserTestTimeoutError') {
    console.log('Request timed out - try simplifying or increasing timeout');
  }
}
```

## Error Handling Strategies

### Basic Error Handling

```javascript theme={null}
async function safeScreenshot(url, options = {}) {
  try {
    const result = await bt.screenshot.take({ url, ...options });
    return { success: true, data: result.data };
  } catch (error) {
    console.error(`Screenshot failed for ${url}:`, error.message);
    return { success: false, error: error.message };
  }
}
```

### Retry Logic

```javascript theme={null}
async function screenshotWithRetry(url, options = {}, maxRetries = 3) {
  let lastError;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      console.log(`Screenshot attempt ${attempt}/${maxRetries} for ${url}`);
      const result = await bt.screenshot.take({
        url,
        timeout: 15000,
        ...options
      });
      return { success: true, data: result.data, attempt };

    } catch (error) {
      lastError = error;
      console.warn(`Attempt ${attempt} failed:`, error.message);

      // Don't retry auth errors
      if (error.name === 'BrowserTestAuthError') {
        throw error;
      }

      if (attempt < maxRetries) {
        // Exponential backoff
        const delay = Math.pow(2, attempt) * 1000;
        console.log(`Waiting ${delay}ms before retry...`);
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }

  throw new Error(`Screenshot failed after ${maxRetries} attempts: ${lastError.message}`);
}
```

### Circuit Breaker Pattern

```javascript theme={null}
class CircuitBreaker {
  constructor(failureThreshold = 5, recoveryTimeout = 60000) {
    this.failureThreshold = failureThreshold;
    this.recoveryTimeout = recoveryTimeout;
    this.failureCount = 0;
    this.lastFailureTime = null;
    this.state = 'closed'; // closed, open, half-open
  }

  async execute(operation) {
    if (this.state === 'open') {
      if (Date.now() - this.lastFailureTime > this.recoveryTimeout) {
        this.state = 'half-open';
      } else {
        throw new Error('Circuit breaker is open');
      }
    }

    try {
      const result = await operation();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failureCount = 0;
    this.state = 'closed';
  }

  onFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();

    if (this.failureCount >= this.failureThreshold) {
      this.state = 'open';
    }
  }
}

// Usage
const circuitBreaker = new CircuitBreaker();

async function protectedScreenshot(url) {
  return circuitBreaker.execute(async () => {
    return bt.screenshot.take({ url });
  });
}
```

### Batch Error Handling

```javascript theme={null}
async function batchScreenshotsWithErrorHandling(urls, options = {}) {
  const results = [];
  const errors = [];

  // Process in smaller chunks to handle failures gracefully
  const chunkSize = 5;

  for (let i = 0; i < urls.length; i += chunkSize) {
    const chunk = urls.slice(i, i + chunkSize);
    console.log(`Processing chunk ${Math.floor(i / chunkSize) + 1}/${Math.ceil(urls.length / chunkSize)}`);

    try {
      const batchResult = await bt.screenshot.takeBatch({
        urls: chunk,
        ...options
      });

      results.push(...batchResult.results.filter(r => r.success));
      errors.push(...batchResult.results.filter(r => !r.success).map(r => ({
        url: r.url,
        error: r.error
      })));

    } catch (error) {
      // If entire batch fails, add all URLs to errors
      errors.push(...chunk.map(url => ({
        url,
        error: error.message
      })));
    }

    // Brief pause between chunks
    await new Promise(resolve => setTimeout(resolve, 1000));
  }

  return {
    successful: results,
    failed: errors,
    summary: {
      total: urls.length,
      successful: results.length,
      failed: errors.length,
      successRate: (results.length / urls.length * 100).toFixed(1) + '%'
    }
  };
}
```

## Async Job Error Handling

### Job Status Monitoring

```javascript theme={null}
async function monitorJobWithErrorHandling(jobId, options = {}) {
  const {
    maxWaitTime = 300000, // 5 minutes
    pollInterval = 5000,   // Check every 5 seconds
    onProgress = null
  } = options;

  const startTime = Date.now();

  while (Date.now() - startTime < maxWaitTime) {
    try {
      const status = await bt.testing.getStatus(jobId);

      if (onProgress) {
        onProgress(status);
      }

      switch (status.job.status) {
        case 'completed':
          return {
            success: true,
            results: status.job.results,
            duration: Date.now() - startTime
          };

        case 'failed':
          return {
            success: false,
            error: status.job.error,
            duration: Date.now() - startTime
          };

        case 'running':
          // Continue monitoring
          break;

        case 'pending':
          // Still queued
          break;
      }

    } catch (error) {
      console.error(`Error checking job status:`, error.message);
      // Continue monitoring despite status check errors
    }

    await new Promise(resolve => setTimeout(resolve, pollInterval));
  }

  throw new Error(`Job ${jobId} monitoring timed out after ${maxWaitTime / 1000} seconds`);
}
```

### Job Creation with Validation

```javascript theme={null}
async function createJobSafely(testOptions) {
  // Validate inputs
  if (!testOptions.instructions || !testOptions.url) {
    throw new Error('Instructions and URL are required');
  }

  if (testOptions.instructions.length > 10000) {
    throw new Error('Instructions too long (max 10000 characters)');
  }

  try {
    // Check connection first
    const isConnected = await bt.testConnection();
    if (!isConnected) {
      throw new Error('API connection failed');
    }

    // Check quota
    const usage = await bt.getUsage();
    if (usage.usage.agentic.remaining <= 0) {
      throw new Error('No agentic test quota remaining');
    }

    const job = await bt.testing.create(testOptions);

    return {
      success: true,
      jobId: job.jobId,
      status: job.status
    };

  } catch (error) {
    console.error('Job creation failed:', error.message);

    return {
      success: false,
      error: error.message,
      canRetry: !['BrowserTestAuthError', 'BrowserTestQuotaError'].includes(error.name)
    };
  }
}
```

## Comprehensive Error Handling Class

```javascript theme={null}
class BrowserTestErrorHandler {
  constructor(bt) {
    this.bt = bt;
    this.errors = [];
    this.maxErrors = 100; // Keep last 100 errors
  }

  logError(operation, error, context = {}) {
    const errorEntry = {
      timestamp: new Date().toISOString(),
      operation,
      error: {
        name: error.name,
        message: error.message,
        status: error.status,
        quotaInfo: error.quotaInfo
      },
      context
    };

    this.errors.push(errorEntry);

    // Keep only recent errors
    if (this.errors.length > this.maxErrors) {
      this.errors = this.errors.slice(-this.maxErrors);
    }

    console.error(`BrowserTest Error [${operation}]:`, error.message);
  }

  async executeWithHandling(operation, operationName, context = {}) {
    try {
      const result = await operation();
      return { success: true, result };
    } catch (error) {
      this.logError(operationName, error, context);

      // Handle specific errors
      switch (error.name) {
        case 'BrowserTestAuthError':
          return {
            success: false,
            error: 'Authentication failed - please check your API key',
            canRetry: false,
            action: 'check_api_key'
          };

        case 'BrowserTestQuotaError':
          return {
            success: false,
            error: `Quota exceeded. Used: ${error.quotaInfo?.usage?.agentic?.used || 'unknown'}`,
            canRetry: false,
            action: 'upgrade_plan'
          };

        case 'BrowserTestTimeoutError':
          return {
            success: false,
            error: 'Operation timed out - try again or increase timeout',
            canRetry: true,
            action: 'retry'
          };

        case 'BrowserTestAPIError':
          const canRetry = error.status >= 500; // Retry server errors
          return {
            success: false,
            error: `API error (${error.status}): ${error.message}`,
            canRetry,
            action: canRetry ? 'retry' : 'contact_support'
          };

        default:
          return {
            success: false,
            error: error.message,
            canRetry: true,
            action: 'retry'
          };
      }
    }
  }

  async executeWithRetry(operation, operationName, options = {}) {
    const {
      maxRetries = 3,
      retryDelay = 1000,
      backoffMultiplier = 2,
      context = {}
    } = options;

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      const result = await this.executeWithHandling(operation, operationName, {
        ...context,
        attempt,
        maxRetries
      });

      if (result.success) {
        return result;
      }

      if (!result.canRetry || attempt === maxRetries) {
        return result;
      }

      const delay = retryDelay * Math.pow(backoffMultiplier, attempt - 1);
      console.log(`Retrying ${operationName} in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  getErrorSummary() {
    const summary = {
      total: this.errors.length,
      byType: {},
      byOperation: {},
      recent: this.errors.slice(-10)
    };

    this.errors.forEach(error => {
      summary.byType[error.error.name] = (summary.byType[error.error.name] || 0) + 1;
      summary.byOperation[error.operation] = (summary.byOperation[error.operation] || 0) + 1;
    });

    return summary;
  }

  clearErrors() {
    this.errors = [];
  }
}

// Usage
const errorHandler = new BrowserTestErrorHandler(bt);

async function safeScreenshot(url) {
  const result = await errorHandler.executeWithRetry(
    () => bt.screenshot.take({ url }),
    'screenshot',
    {
      url,
      maxRetries: 3,
      context: { url }
    }
  );

  if (!result.success) {
    console.error('Screenshot failed:', result.error);
    return null;
  }

  return result.result;
}
```

## Best Practices

### Error Classification

```javascript theme={null}
function classifyError(error) {
  // Transient errors that should be retried
  const transientErrors = [
    'BrowserTestTimeoutError',
    'ECONNRESET',
    'ETIMEDOUT',
    'ENOTFOUND'
  ];

  // Permanent errors that shouldn't be retried
  const permanentErrors = [
    'BrowserTestAuthError',
    'BrowserTestQuotaError'
  ];

  // API errors based on status
  if (error.name === 'BrowserTestAPIError') {
    if (error.status >= 400 && error.status < 500) {
      return 'client_error'; // Don't retry
    } else if (error.status >= 500) {
      return 'server_error'; // Retry
    }
  }

  if (transientErrors.includes(error.name) || transientErrors.includes(error.code)) {
    return 'transient';
  }

  if (permanentErrors.includes(error.name)) {
    return 'permanent';
  }

  return 'unknown';
}
```

### Graceful Degradation

```javascript theme={null}
class GracefulBrowserTest {
  constructor(bt) {
    this.bt = bt;
    this.fallbackMode = false;
  }

  async screenshot(url, options = {}) {
    try {
      if (this.fallbackMode) {
        return this.fallbackScreenshot(url, options);
      }

      const result = await this.bt.screenshot.take({ url, ...options });
      return result;

    } catch (error) {
      console.warn('Primary screenshot failed, trying fallback:', error.message);

      if (!this.fallbackMode) {
        this.fallbackMode = true;
        return this.fallbackScreenshot(url, options);
      }

      throw error;
    }
  }

  async fallbackScreenshot(url, options = {}) {
    // Simplified fallback implementation
    // Could use a different service or cached results
    console.log('Using fallback screenshot method for', url);

    try {
      // Try with minimal options
      const result = await this.bt.screenshot.take({
        url,
        format: 'png',
        timeout: 30000,
        ...options
      });

      this.fallbackMode = false; // Reset on success
      return result;

    } catch (fallbackError) {
      console.error('Fallback also failed:', fallbackError.message);
      throw fallbackError;
    }
  }
}
```

### Monitoring and Alerting

```javascript theme={null}
class ErrorMonitor {
  constructor(bt, options = {}) {
    this.bt = bt;
    this.alertThreshold = options.alertThreshold || 5;
    this.alertWindow = options.alertWindow || 300000; // 5 minutes
    this.errorCounts = new Map();
    this.lastAlert = 0;
  }

  recordError(error, operation) {
    const key = `${error.name}:${operation}`;
    const now = Date.now();

    if (!this.errorCounts.has(key)) {
      this.errorCounts.set(key, { count: 0, firstSeen: now });
    }

    const errorInfo = this.errorCounts.get(key);
    errorInfo.count++;
    errorInfo.lastSeen = now;

    // Clean old entries
    for (const [k, v] of this.errorCounts) {
      if (now - v.lastSeen > this.alertWindow) {
        this.errorCounts.delete(k);
      }
    }

    // Check if we should alert
    if (errorInfo.count >= this.alertThreshold &&
        now - this.lastAlert > this.alertWindow) {
      this.sendAlert(error, operation, errorInfo);
      this.lastAlert = now;
    }
  }

  sendAlert(error, operation, errorInfo) {
    const alert = {
      message: `High error rate detected: ${error.name} in ${operation}`,
      count: errorInfo.count,
      timeWindow: this.alertWindow / 1000 + 's',
      firstSeen: new Date(errorInfo.firstSeen).toISOString(),
      lastSeen: new Date(errorInfo.lastSeen).toISOString()
    };

    console.error('🚨 ERROR ALERT:', alert);
    // In production, send to monitoring service
    // sendToMonitoringService(alert);
  }

  getStats() {
    const stats = {
      totalErrors: 0,
      byType: {},
      byOperation: {}
    };

    for (const [key, info] of this.errorCounts) {
      const [errorType, operation] = key.split(':');
      stats.totalErrors += info.count;
      stats.byType[errorType] = (stats.byType[errorType] || 0) + info.count;
      stats.byOperation[operation] = (stats.byOperation[operation] || 0) + info.count;
    }

    return stats;
  }
}

// Usage
const monitor = new ErrorMonitor(bt);

async function monitoredOperation() {
  try {
    return await bt.screenshot.take({ url: 'https://example.com' });
  } catch (error) {
    monitor.recordError(error, 'screenshot');
    throw error;
  }
}
```
