Skip to main content

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.
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.
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.).
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.
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

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

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

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

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

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

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

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

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

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

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;
  }
}