How to Capture the Current Page After a Puppeteer Timeout
Catch Puppeteer timeouts, keep the same page, and save its screenshot, HTML, URL, title, and error safely.
Yes. A Puppeteer timeout does not automatically invalidate the Page object. Catch the navigation or wait error, keep the same page, record its URL, title, and HTML, then call page.screenshot(). Save the original timeout alongside the artifacts so a partial page remains useful for debugging.
1. Capture the page after a timeout
This complete script waits for the initial DOM, preserves timeout details, writes an HTML snapshot and screenshot, and records any second error from the capture itself.
const puppeteer = require('puppeteer');
const fs = require('fs');
const targetUrl = process.argv[2] || 'https://example.com';
(async () => {
const browser = await puppeteer.launch({headless: true});
const page = await browser.newPage();
let navigationError = null;
let captureError = null;
try {
await page.goto(targetUrl, {waitUntil: 'domcontentloaded', timeout: 15000});
} catch (error) {
navigationError = {name: error.name, message: error.message, stack: error.stack};
}
const snapshot = {navigationError, url: page.url(), title: null};
try {
snapshot.title = await page.title();
fs.writeFileSync('puppeteer-timeout.html', await page.content(), 'utf8');
await page.screenshot({path: 'puppeteer-timeout.png', fullPage: true});
} catch (error) {
captureError = {name: error.name, message: error.message, stack: error.stack};
snapshot.captureError = captureError;
} finally {
fs.writeFileSync('puppeteer-timeout.json', JSON.stringify(snapshot, null, 2));
await browser.close();
}
if (navigationError) process.exitCode = 2;
})();
Install Puppeteer with npm install puppeteer, save the file as capture-timeout.js, and run node capture-timeout.js https://your-site.example. The process reports the timeout while still leaving the files on disk.
2. What the timeout means
A timeout means that the selected operation did not satisfy its waiting condition before its limit. It does not, by itself, mean that the Page is unusable. page.content() returns the serialized HTML, including the DOCTYPE, while page.screenshot() captures the current rendered page. The result may be incomplete if scripts, images, or redirects were still in flight.
Puppeteer’s navigation timeout covers goBack(), goForward(), goto(), reload(), setContent(), and waitForNavigation(). Selector waits use a 30-second default and accept timeout: 0 for an unlimited wait. See the screenshot API, content API, and navigation-timeout API.
3. Choose the right artifact
| Artifact | Best for | Limitation |
|---|---|---|
| Screenshot | What a human could see at timeout; use fullPage: true for the entire scrollable document. |
Does not preserve DOM structure or selectable text. |
| HTML | DOM inspection, parsing, and text extraction. | Does not preserve pixels, fonts, or final layout. |
| URL and title | Identifying redirects and the final document. | Either can be empty during an early failure. |
| Error and stack | Correlating artifacts with the failed wait. | Do not replace the original message with a generic error. |
4. Prevent avoidable timeouts
Match the readiness condition
domcontentloadedis useful when the initial DOM is enough.loadwaits for the load event and more resources.networkidle0andnetworkidle2can be unsuitable for polling pages, analytics, WebSockets, or other long-lived requests.- A selector wait is best when a known component marks readiness.
await page.goto(url, {waitUntil: 'domcontentloaded', timeout: 15000});
await page.waitForSelector('[data-ready="true"]', {timeout: 8000});
page.setDefaultNavigationTimeout(30000);
Pair clicks with navigation waits
Register the navigation wait before clicking. A separate click followed by a wait can race and miss a fast navigation.
const [response] = await Promise.all([
page.waitForNavigation({waitUntil: 'domcontentloaded', timeout: 15000}),
page.click('a.next'),
]);
waitForNavigation() covers navigation, reloads, and History API URL changes. If a click only updates the DOM, wait for the resulting selector instead.
5. Capture partial state deliberately
Use the same page reference. Creating a new page after the timeout discards the partial state you are trying to inspect.
let navigationError;
try {
await page.goto(targetUrl, {waitUntil: 'domcontentloaded', timeout: 15000});
} catch (error) {
navigationError = error;
}
const snapshot = {
error: navigationError?.message ?? null,
url: page.url(),
title: await page.title(),
html: await page.content(),
};
await page.screenshot({path: 'puppeteer-timeout.png', fullPage: true});
For very large pages, write HTML directly to a file instead of retaining multiple copies in memory. If a full-page image is too tall, capture the viewport or bounded sections.
6. Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
No screenshot after goto timed out |
Capture was never attempted or the browser closed in the catch block. | Capture after the catch and close only in finally. |
| Blank screenshot | The page is still blank, on an interstitial, or the renderer failed. | Save URL, title, HTML, and error; retry once with a longer finite timeout. |
| HTML is an error page | The server, proxy, or bot check returned an error document. | Keep it as diagnostic evidence and inspect authentication and response details. |
| Full-page capture throws | Extreme document dimensions, a closed target, or renderer failure. | Capture the viewport or bounded regions and preserve the capture error. |
waitForSelector times out |
Wrong selector, client rendering, or a state that never occurs. | Verify the selector in saved HTML or wait for a stable parent element. |
| Click navigation is missed | The click happened before the navigation listener was registered. | Use the Promise.all pattern with the wait first. |
| Files are incomplete | Cleanup ran before asynchronous writes finished. | Await every write and screenshot before closing the browser. |
7. Reliability, performance, and cost
- Reliability: preserve navigation and capture errors, URL, title, timeout values, and timestamps beside artifacts.
- Performance: DOM-content readiness is generally faster than network-idle waits. Full-page screenshots and large HTML consume more memory.
- Timeout policy: prefer finite, operation-specific limits. An unlimited wait can stall a worker indefinitely.
- Retries: retry only when the failure may be transient, and label each attempt because a retry can produce different content.
- Cost: Puppeteer has no screenshot API charge, but your runtime still consumes CPU, memory, storage, and bandwidth.
8. Or skip the browser setup
ScreenshotNeo provides a GET screenshot API and an MCP server. Before capture it accepts consent banners and removes 60+ known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and responses identify the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
See the ScreenshotNeo API documentation for options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
The free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.
9. FAQ
Can I get HTML after waitForNavigation() fails?
Yes. Catch the error and call page.content() on the same page. The result may be partial or an error document.
Should I reload before taking the screenshot?
Usually no. Reloading discards the partial state. Save the current page first, then make a separately labeled retry.
Which wait works for a page that never becomes idle?
Use domcontentloaded plus a selector for the component you need.
Can a timeout happen after useful content appears?
Yes. A late network or selector condition can fail even when the page is visibly rendered, which is why capture should run after the catch.


