Skip to main content

Basic Error Handling

Try-Catch with Specific Error Types

import { BrowserTest } from 'browsertest-sdk';

const bt = new BrowserTest({
  apiKey: process.env.BROWSERTEST_API_KEY
});

async function safeScreenshot(url) {
  try {
    const result = await bt.screenshot.take({ url });
    console.log('Screenshot successful:', result.meta.size, 'bytes');
    return result;
  } catch (error) {
    // Handle different error types
    if (error.name === 'BrowserTestAuthError') {
      console.error('Authentication failed - check your API key');
      // Prompt user to update API key
      return null;
    }

    if (error.name === 'BrowserTestQuotaError') {
      console.error('Quota exceeded:', error.quotaInfo?.usage.screenshot.remaining, 'screenshots remaining');
      // Suggest upgrading plan or waiting
      return null;
    }

    if (error.name === 'BrowserTestTimeoutError') {
      console.error('Request timed out - try again or increase timeout');
      // Retry with longer timeout
      return null;
    }

    if (error.name === 'BrowserTestAPIError') {
      console.error('API error:', error.status, error.message);
      // Handle based on status code
      return null;
    }

    // Unknown error
    console.error('Unexpected error:', error.message);
    return null;
  }
}

// Usage
const result = await safeScreenshot('https://example.com');
if (result) {
  // Process successful result
  console.log('Success!');
}

Graceful Degradation

class GracefulBrowserTest {
  constructor(config) {
    this.bt = new BrowserTest(config);
    this.fallbackMode = false;
  }

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

      const result = await this.bt.screenshot.take({ url, ...options });
      this.fallbackMode = false; // Reset on success
      return result;

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

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

      throw error;
    }
  }

  async fallbackScreenshot(url) {
    // Simple fallback - just get page info
    try {
      const response = await fetch(url);
      const html = await response.text();
      const title = html.match(/<title>(.*?)<\/title>/)?.[1] || 'Unknown';

      return {
        success: true,
        data: {
          screenshot: '', // No image
          url,
          title,
          width: 0,
          height: 0,
          format: '',
          fullPage: false
        },
        meta: {
          timeMs: 0,
          size: 0
        }
      };
    } catch (fallbackError) {
      throw new Error(`Both primary and fallback methods failed: ${fallbackError.message}`);
    }
  }
}

// Usage
const gbt = new GracefulBrowserTest({
  apiKey: process.env.BROWSERTEST_API_KEY
});

const result = await gbt.screenshot('https://example.com');
// Works even if API is down (returns basic page info)

Retry Mechanisms

Exponential Backoff Retry

class RetryHandler {
  constructor(maxRetries = 3, baseDelay = 1000) {
    this.maxRetries = maxRetries;
    this.baseDelay = baseDelay;
  }

  async executeWithRetry(operation, context = {}) {
    let lastError;

    for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
      try {
        console.log(`Attempt ${attempt}/${this.maxRetries}`);
        const result = await operation();

        if (attempt > 1) {
          console.log(`Succeeded on attempt ${attempt}`);
        }

        return result;

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

        // Don't retry certain errors
        if (this.shouldNotRetry(error)) {
          console.log('Not retrying this type of error');
          throw error;
        }

        if (attempt === this.maxRetries) {
          console.error(`All ${this.maxRetries} attempts failed`);
          throw lastError;
        }

        // Exponential backoff with jitter
        const delay = this.baseDelay * Math.pow(2, attempt - 1);
        const jitter = Math.random() * 1000; // Up to 1 second jitter
        const totalDelay = delay + jitter;

        console.log(`Waiting ${Math.round(totalDelay)}ms before retry...`);
        await new Promise(resolve => setTimeout(resolve, totalDelay));
      }
    }
  }

  shouldNotRetry(error) {
    // Don't retry auth or quota errors
    return ['BrowserTestAuthError', 'BrowserTestQuotaError'].includes(error.name);
  }
}

// Usage
const retryHandler = new RetryHandler(3, 1000);

const result = await retryHandler.executeWithRetry(
  () => bt.screenshot.take({ url: 'https://unreliable-site.com' }),
  { operation: 'screenshot', url: 'https://unreliable-site.com' }
);

Circuit Breaker Pattern

class CircuitBreaker {
  constructor(failureThreshold = 5, recoveryTimeout = 60000, monitoringPeriod = 10000) {
    this.failureThreshold = failureThreshold;
    this.recoveryTimeout = recoveryTimeout;
    this.monitoringPeriod = monitoringPeriod;

    this.failureCount = 0;
    this.lastFailureTime = null;
    this.state = 'closed'; // closed, open, half-open

    // Periodic health check
    setInterval(() => this.attemptRecovery(), monitoringPeriod);
  }

  async execute(operation) {
    if (this.state === 'open') {
      throw new Error('Circuit breaker is OPEN - service unavailable');
    }

    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';
      console.log(`Circuit breaker opened after ${this.failureCount} failures`);
    }
  }

  async attemptRecovery() {
    if (this.state === 'open' && this.lastFailureTime) {
      const timeSinceFailure = Date.now() - this.lastFailureTime;

      if (timeSinceFailure >= this.recoveryTimeout) {
        console.log('Attempting circuit breaker recovery...');
        this.state = 'half-open';
        this.failureCount = 0;
      }
    }
  }

  getState() {
    return {
      state: this.state,
      failureCount: this.failureCount,
      lastFailureTime: this.lastFailureTime
    };
  }
}

// Usage
const circuitBreaker = new CircuitBreaker(3, 30000); // 3 failures, 30 second recovery

async function safeOperation() {
  try {
    return await circuitBreaker.execute(
      () => bt.screenshot.take({ url: 'https://example.com' })
    );
  } catch (error) {
    if (error.message.includes('Circuit breaker is OPEN')) {
      console.log('Service temporarily unavailable, using cache or fallback');
      return getCachedOrFallbackResult();
    }
    throw error;
  }
}

Batch Error Handling

Partial Success Handling

async function batchWithPartialSuccess(urls, options = {}) {
  const results = {
    successful: [],
    failed: [],
    summary: {
      total: urls.length,
      successful: 0,
      failed: 0,
      successRate: 0
    }
  };

  // Process all requests, collecting results
  const promises = urls.map(async (url) => {
    try {
      const result = await bt.screenshot.take({ url, ...options });
      results.successful.push(result);
      return { url, success: true, result };
    } catch (error) {
      const failure = {
        url,
        error: error.message,
        type: error.name,
        timestamp: new Date().toISOString()
      };
      results.failed.push(failure);
      return { url, success: false, error: failure };
    }
  });

  await Promise.allSettled(promises);

  // Calculate summary
  results.summary.successful = results.successful.length;
  results.summary.failed = results.failed.length;
  results.summary.successRate = (results.successful.length / urls.length) * 100;

  return results;
}

// Usage
const batchResults = await batchWithPartialSuccess([
  'https://site1.com',
  'https://site2.com',
  'https://broken-site.com',
  'https://site3.com'
], { fullPage: true });

console.log(`Batch complete: ${batchResults.summary.successRate.toFixed(1)}% success rate`);
console.log(`Successful: ${batchResults.summary.successful}`);
console.log(`Failed: ${batchResults.summary.failed}`);

// Process successful results
batchResults.successful.forEach(result => {
  // Save or process result
  saveScreenshot(result.data.screenshot, result.data.url);
});

// Log failures for analysis
batchResults.failed.forEach(failure => {
  console.error(`Failed to screenshot ${failure.url}: ${failure.error}`);
  // Could retry individually or log for later analysis
});

Batch Retry Logic

async function batchWithRetry(urls, options = {}, retryOptions = {}) {
  const {
    maxRetries = 2,
    retryFailedOnly = true,
    concurrency = 3
  } = retryOptions;

  let currentUrls = [...urls];
  let allResults = { successful: [], failed: [] };

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    console.log(`Batch attempt ${attempt}/${maxRetries} for ${currentUrls.length} URLs`);

    const batchResults = await processBatch(currentUrls, options, concurrency);

    // Add successful results to final collection
    allResults.successful.push(...batchResults.successful);

    // Prepare failed URLs for retry (if enabled)
    if (retryFailedOnly && batchResults.failed.length > 0) {
      console.log(`${batchResults.failed.length} URLs failed, preparing for retry...`);
      currentUrls = batchResults.failed.map(f => f.url);
    } else {
      // Add remaining failures to final collection
      allResults.failed.push(...batchResults.failed);
      break;
    }

    // Don't retry on last attempt
    if (attempt === maxRetries) {
      allResults.failed.push(...batchResults.failed);
    }
  }

  return allResults;
}

async function processBatch(urls, options, concurrency) {
  const results = { successful: [], failed: [] };

  // Simple concurrency control
  for (let i = 0; i < urls.length; i += concurrency) {
    const batch = urls.slice(i, i + concurrency);
    const batchPromises = batch.map(async (url) => {
      try {
        const result = await bt.screenshot.take({ url, ...options });
        results.successful.push(result);
        return { url, success: true, result };
      } catch (error) {
        const failure = { url, error: error.message, type: error.name };
        results.failed.push(failure);
        return { url, success: false, error: failure };
      }
    });

    await Promise.all(batchPromises);
  }

  return results;
}

// Usage
const finalResults = await batchWithRetry(
  ['https://site1.com', 'https://site2.com', 'https://unreliable.com'],
  { fullPage: true },
  { maxRetries: 2, retryFailedOnly: true, concurrency: 2 }
);

console.log(`Final results: ${finalResults.successful.length} successful, ${finalResults.failed.length} failed`);

Monitoring and Alerting

Error Tracking and Reporting

class ErrorTracker {
  constructor() {
    this.errors = [];
    this.maxErrors = 1000; // Keep last 1000 errors
    this.alerts = new Map();
  }

  trackError(error, context = {}) {
    const errorEntry = {
      timestamp: new Date().toISOString(),
      error: {
        name: error.name,
        message: error.message,
        stack: error.stack
      },
      context,
      userAgent: navigator?.userAgent,
      url: window?.location?.href
    };

    this.errors.push(errorEntry);

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

    // Check for alert conditions
    this.checkAlerts(error, context);

    // Report to monitoring service
    this.reportError(errorEntry);
  }

  checkAlerts(error, context) {
    const errorKey = `${error.name}:${context.operation || 'unknown'}`;
    const now = Date.now();
    const windowMs = 5 * 60 * 1000; // 5 minutes

    // Count errors in the last 5 minutes
    const recentErrors = this.errors.filter(e =>
      now - new Date(e.timestamp).getTime() < windowMs &&
      `${e.error.name}:${e.context.operation}` === errorKey
    );

    if (recentErrors.length >= 5) {
      const alertKey = `high_error_rate_${errorKey}`;
      if (!this.alerts.has(alertKey) || now - this.alerts.get(alertKey) > windowMs) {
        this.sendAlert({
          type: 'high_error_rate',
          errorType: error.name,
          operation: context.operation,
          count: recentErrors.length,
          timeWindow: '5 minutes'
        });
        this.alerts.set(alertKey, now);
      }
    }
  }

  sendAlert(alert) {
    console.error('🚨 ERROR ALERT:', alert);

    // In production, send to monitoring service
    // sendToMonitoringService(alert);
  }

  async reportError(errorEntry) {
    try {
      // Send to error reporting service
      // await fetch('/api/errors', {
      //   method: 'POST',
      //   headers: { 'Content-Type': 'application/json' },
      //   body: JSON.stringify(errorEntry)
      // });
    } catch (reportError) {
      console.error('Failed to report error:', reportError);
    }
  }

  getErrorSummary(timeRangeMs = 60 * 60 * 1000) { // Last hour
    const cutoff = new Date(Date.now() - timeRangeMs);
    const recentErrors = this.errors.filter(e => new Date(e.timestamp) > cutoff);

    const summary = {
      total: recentErrors.length,
      byType: {},
      byOperation: {},
      timeline: this.generateTimeline(recentErrors)
    };

    recentErrors.forEach(error => {
      const type = error.error.name;
      const operation = error.context.operation || 'unknown';

      summary.byType[type] = (summary.byType[type] || 0) + 1;
      summary.byOperation[operation] = (summary.byOperation[operation] || 0) + 1;
    });

    return summary;
  }

  generateTimeline(errors) {
    const timeline = {};
    errors.forEach(error => {
      const minute = new Date(error.timestamp).toISOString().slice(0, 16); // YYYY-MM-DDTHH:MM
      timeline[minute] = (timeline[minute] || 0) + 1;
    });
    return timeline;
  }
}

// Usage
const errorTracker = new ErrorTracker();

async function trackedOperation() {
  try {
    return await bt.screenshot.take({ url: 'https://example.com' });
  } catch (error) {
    errorTracker.trackError(error, {
      operation: 'screenshot',
      url: 'https://example.com',
      userId: 'user123'
    });
    throw error;
  }
}

// Get error summary
const summary = errorTracker.getErrorSummary();
console.log('Error summary:', summary);

Health Check with Error Context

class HealthChecker {
  constructor(bt) {
    this.bt = bt;
    this.lastHealthCheck = null;
    this.healthStatus = 'unknown';
  }

  async performHealthCheck() {
    const startTime = Date.now();
    const checks = {
      connection: false,
      screenshot: false,
      test: false,
      usage: false
    };

    try {
      // Test basic connection
      const isConnected = await this.bt.testConnection();
      checks.connection = isConnected;

      if (isConnected) {
        // Test screenshot functionality
        try {
          await this.bt.screenshot.take({
            url: 'https://httpbin.org/html',
            timeout: 5000
          });
          checks.screenshot = true;
        } catch (error) {
          checks.screenshot = false;
          console.warn('Screenshot health check failed:', error.message);
        }

        // Test testing functionality
        try {
          await this.bt.testing.execute({
            instructions: 'Navigate to page',
            url: 'https://httpbin.org/html',
            config: { timeout: 5000 }
          });
          checks.test = true;
        } catch (error) {
          checks.test = false;
          console.warn('Test health check failed:', error.message);
        }

        // Test usage API
        try {
          await this.bt.getUsage();
          checks.usage = true;
        } catch (error) {
          checks.usage = false;
          console.warn('Usage health check failed:', error.message);
        }
      }

    } catch (error) {
      console.error('Health check failed:', error.message);
    }

    const duration = Date.now() - startTime;
    const allHealthy = Object.values(checks).every(Boolean);

    this.lastHealthCheck = {
      timestamp: new Date().toISOString(),
      status: allHealthy ? 'healthy' : 'degraded',
      duration,
      checks
    };

    this.healthStatus = this.lastHealthCheck.status;

    return this.lastHealthCheck;
  }

  getHealthStatus() {
    return {
      status: this.healthStatus,
      lastCheck: this.lastHealthCheck,
      timeSinceLastCheck: this.lastHealthCheck ?
        Date.now() - new Date(this.lastHealthCheck.timestamp).getTime() : null
    };
  }

  async waitForHealthy(timeoutMs = 30000) {
    const startTime = Date.now();

    while (Date.now() - startTime < timeoutMs) {
      const health = await this.performHealthCheck();

      if (health.status === 'healthy') {
        return health;
      }

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

    throw new Error(`Service did not become healthy within ${timeoutMs}ms`);
  }
}

// Usage
const healthChecker = new HealthChecker(bt);

// Perform health check
const health = await healthChecker.performHealthCheck();
console.log('Health status:', health.status);
console.log('Checks:', health.checks);

// Wait for service to be healthy
try {
  await healthChecker.waitForHealthy(60000); // Wait up to 1 minute
  console.log('Service is now healthy');
} catch (error) {
  console.error('Service failed to become healthy:', error.message);
}

Custom Error Types and Handling

Domain-Specific Errors

class ScreenshotError extends Error {
  constructor(message, public url, public options = {}) {
    super(message);
    this.name = 'ScreenshotError';
  }
}

class ValidationError extends Error {
  constructor(message, public field, public value) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
    this.value = value;
  }
}

class QuotaExceededError extends Error {
  constructor(message, public currentUsage, public limit) {
    super(message);
    this.name = 'QuotaExceededError';
    this.currentUsage = currentUsage;
    this.limit = limit;
  }
}

// Enhanced error handler
class EnhancedErrorHandler {
  constructor() {
    this.errorHandlers = new Map();
  }

  registerHandler(errorType, handler) {
    this.errorHandlers.set(errorType, handler);
  }

  async handleError(error, context = {}) {
    const handler = this.errorHandlers.get(error.name) || this.defaultHandler;
    return handler(error, context);
  }

  async defaultHandler(error, context) {
    console.error('Unhandled error:', error.message, context);
    throw error; // Re-throw by default
  }
}

// Register handlers
const errorHandler = new EnhancedErrorHandler();

errorHandler.registerHandler('BrowserTestQuotaError', async (error, context) => {
  console.log('Quota exceeded, attempting graceful handling');

  // Check if we can continue with reduced functionality
  if (context.allowReducedFunctionality) {
    console.log('Continuing with reduced functionality');
    return { action: 'continue_reduced', error };
  }

  // Suggest upgrade
  return {
    action: 'upgrade_required',
    message: 'Please upgrade your plan to continue',
    error
  };
});

errorHandler.registerHandler('BrowserTestTimeoutError', async (error, context) => {
  if (context.retryCount < 3) {
    console.log('Retrying after timeout...');
    return { action: 'retry', error };
  }

  return { action: 'fail', error };
});

// Usage
async function robustOperation() {
  const context = { allowReducedFunctionality: true, retryCount: 0 };

  try {
    return await bt.screenshot.take({ url: 'https://example.com' });
  } catch (error) {
    const result = await errorHandler.handleError(error, context);

    switch (result.action) {
      case 'retry':
        context.retryCount++;
        return robustOperation(); // Recursive retry

      case 'continue_reduced':
        return performReducedFunctionality();

      case 'upgrade_required':
        throw new Error('Service upgrade required');

      default:
        throw result.error;
    }
  }
}
This comprehensive error handling approach ensures your BrowserTest integration is robust, maintainable, and provides excellent user experience even when things go wrong.