Basic Error Handling
Try-Catch with Specific Error Types
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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;
}
}
}
