Screenshot API Error Handling: Best Practices
Screenshot APIs interact with the open web, which means things can and will go wrong. Target sites go down, pages take too long to load, rate limits get hit, and network connections fail. Building robust error handling from day one saves you from debugging production incidents at 3am.
Understanding HTTP Status Codes
ScreenshotAPI returns standard HTTP status codes. Here is what each category means for your integration:
| Code | Meaning | Action |
|---|---|---|
| 200 | Success | Save the image/PDF |
| 400 | Bad Request | Fix request parameters |
| 401 | Unauthorized | Check API key |
| 403 | Forbidden | URL blocked (SSRF protection) |
| 404 | Not Found | Target page does not exist |
| 429 | Rate Limited | Wait and retry with backoff |
| 500 | Server Error | Retry after delay |
| 504 | Gateway Timeout | Page too slow; increase wait or use async |
Retry Strategy with Exponential Backoff
Not all errors are permanent. Network blips and temporary server issues resolve on their own. Implement exponential backoff to handle transient failures:
async function captureWithRetry(url, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(
`https://screenshotapi-api-production.up.railway.app/v1/screenshot?url=${encodeURIComponent(url)}`,
{ headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);
if (response.ok) {
return await response.arrayBuffer();
}
// Don't retry client errors (except 429)
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
throw new Error(`Client error ${response.status}: ${await response.text()}`);
}
// For 429, respect Retry-After header
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
console.log(`Rate limited. Waiting ${retryAfter}s...`);
await sleep(retryAfter * 1000);
continue;
}
// Server error: retry with backoff
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
console.log(`Attempt ${attempt} failed (${response.status}). Retrying in ${delay}ms...`);
await sleep(delay);
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = Math.pow(2, attempt) * 1000;
await sleep(delay);
}
}
throw new Error('Max retries exceeded');
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}Timeout Handling
Some websites take a very long time to load -- heavy JavaScript, slow APIs, large images. Set a reasonable timeout on your end to avoid hanging requests:
// Set a 30-second timeout
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000);
try {
const response = await fetch(apiUrl, {
headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
signal: controller.signal,
});
clearTimeout(timeout);
// Process response...
} catch (error) {
clearTimeout(timeout);
if (error.name === 'AbortError') {
console.error('Screenshot request timed out after 30s');
// Use fallback image or queue for async processing
}
}For pages that consistently timeout, consider using the wait parameter to give the page more time to render, or switch to async mode with webhooks.
Fallback Images
When a screenshot fails and retries are exhausted, show a fallback instead of a broken image. This is especially important for customer-facing features like link previews:
async function getScreenshotOrFallback(url) {
try {
const screenshot = await captureWithRetry(url);
return { type: 'screenshot', data: screenshot };
} catch (error) {
console.error(`Screenshot failed for ${url}: ${error.message}`);
return {
type: 'fallback',
data: generateFallbackImage(url), // SVG or canvas with URL text
};
}
}
function generateFallbackImage(url) {
// Generate a simple placeholder with the domain name
const domain = new URL(url).hostname;
return `<svg width="1280" height="800" xmlns="http://www.w3.org/2000/svg">
<rect fill="#1A1A2E" width="100%" height="100%"/>
<text x="50%" y="50%" text-anchor="middle" fill="#94a3b8"
font-family="system-ui" font-size="24">${domain}</text>
</svg>`;
}Rate Limit Management
ScreenshotAPI includes rate limit headers in every response. Use them to pace your requests and avoid hitting limits:
// Check rate limit headers
const remaining = response.headers.get('X-RateLimit-Remaining');
const resetTime = response.headers.get('X-RateLimit-Reset');
if (parseInt(remaining) < 5) {
console.warn(`Only ${remaining} requests remaining. Reset at ${resetTime}`);
// Slow down or queue remaining work
}
// For batch processing: respect limits proactively
async function batchCapture(urls) {
const CONCURRENT = 5; // Don't exceed rate limit
const DELAY_BETWEEN = 200; // 200ms between requests
for (let i = 0; i < urls.length; i += CONCURRENT) {
const batch = urls.slice(i, i + CONCURRENT);
await Promise.all(batch.map(url => captureWithRetry(url)));
if (i + CONCURRENT < urls.length) {
await sleep(DELAY_BETWEEN * CONCURRENT);
}
}
}Async Mode for Heavy Pages
For pages that consistently timeout or when you need to capture many screenshots, use the async endpoint with webhooks. This decouples the request from the response and handles all retry logic server-side:
// Submit async screenshot request
const response = await fetch(
'https://screenshotapi-api-production.up.railway.app/v1/screenshot',
{
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://heavy-js-site.com',
format: 'png',
async: true,
webhook_url: 'https://your-app.com/api/screenshot-callback',
}),
}
);
const { job_id } = await response.json();
// Screenshot will be delivered to your webhook URL when readyLogging and Monitoring
Track error rates and response times to catch issues before they affect users. Log enough context to debug failures without logging sensitive data:
async function captureAndLog(url, context) {
const startTime = Date.now();
try {
const result = await captureWithRetry(url);
const duration = Date.now() - startTime;
console.log(JSON.stringify({
event: 'screenshot_success',
url,
duration_ms: duration,
context,
}));
return result;
} catch (error) {
const duration = Date.now() - startTime;
console.error(JSON.stringify({
event: 'screenshot_failure',
url,
error: error.message,
duration_ms: duration,
context,
}));
throw error;
}
}Error Handling Checklist
- Implement exponential backoff for 429 and 5xx errors
- Set request timeouts (30s recommended)
- Have fallback images ready for failed screenshots
- Monitor rate limit headers to pace batch requests
- Use async mode for unreliable or heavy target sites
- Log all failures with context for debugging
- Never retry 400/401/403 errors (they are permanent)
- Handle network errors separately from HTTP errors
Build robust screenshot workflows
ScreenshotAPI includes rate limit headers, detailed error messages, and async processing out of the box. Start free with 100 screenshots/month.