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.Copy
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.Copy
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.).Copy
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.Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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;
}
}
