How to Fix Puppeteer’s “Target Closed” Screenshot Protocol Error
Puppeteer’s `Page.captureScreenshot: Target closed` means the page target disappeared. Trace page cleanup, pending promises, and Chromium crashes to find the cause.

Puppeteer’s Protocol error (Page.captureScreenshot): Target closed means Chromium’s page target no longer exists when Puppeteer sends or awaits the screenshot command. The most common path to a fix is to find who closed the page, then make sure every page operation—including page.screenshot()—finishes before cleanup. If the error occurs during browser launch or intermittently in a container, investigate Chromium’s process and runtime environment too: the protocol message can hide a browser crash or startup failure.
There is no single delay or launch flag that fixes every instance. Treat “Target closed” as a lifecycle symptom and use the error timing, close events, browser stderr, and version details to identify its cause.
1. What the error means
Puppeteer communicates with Chromium through the Chrome DevTools Protocol (CDP). A page is represented by a target. When code closes the page or its browser context, the browser disconnects, or Chromium exits, that target is gone. A pending Page.captureScreenshot command then has nowhere to run and can fail with “Target closed.” The same wording can also appear for a different protocol command, including during startup.

That makes the error a symptom, not a diagnosis. The timing gives you a useful first split:
| When it happens | First place to investigate |
|---|---|
During page.screenshot() |
Page closure, browser closure, or another operation still running. |
After page.evaluate() or a race between waits |
Unfinished evaluation work, losing promises, and cleanup ordering. |
| At launch or during a run in Docker | Chromium stderr, process exit, libraries, and Node/Puppeteer/Chromium compatibility. |
2. Start with awaited, sequential page work
Make the order explicit: navigate, capture, then close. An await on the screenshot is essential. Without it, cleanup can run while Puppeteer is still sending or waiting for the screenshot command.
const puppeteer = require('puppeteer');
async function capture(url) {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'shot.png', fullPage: true });
await page.close();
} finally {
await browser.close();
}
}
capture('https://example.com').catch(error => {
console.error('Screenshot failed:', error);
process.exitCode = 1;
});
Save this as capture.js, install Puppeteer in the project, and run node capture.js. This example intentionally captures a full page and uses networkidle2; change either choice to suit the page. The important part is the sequencing, not those particular options.
A safer nested cleanup pattern also closes the page if navigation or capture fails. The isClosed() check avoids attempting to close a page that has already gone away.
const puppeteer = require('puppeteer');
async function capture(url) {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
try {
await page.goto(url, { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'shot.png', fullPage: true });
} finally {
if (!page.isClosed()) await page.close();
}
} finally {
await browser.close();
}
}
capture('https://example.com').catch(error => {
console.error(error);
process.exitCode = 1;
});
This pattern guarantees that normal page work completes before its cleanup runs. It cannot prevent an external browser crash or a separate part of your application from closing the browser concurrently.
3. Trace every path that can close the target
Search the application for page.close(), browser.close(), browser-context closure, timeout callbacks, and error handlers. A close may happen in a different function from the screenshot. Add temporary logs at each close site and record the page URL or job identifier so concurrent captures can be distinguished.
These events help establish whether the page closed or the browser disconnected first. Register listeners soon after creating the browser and page:
browser.on('disconnected', () => {
console.error('Browser disconnected');
});
page.on('close', () => {
console.error('Page closed:', page.url());
});
page.on('error', error => {
console.error('Page crashed:', error);
});
Use event names supported by your installed Puppeteer version, and keep logging handlers small. Capture process exit and Chromium stderr separately when investigating container or launch failures. A page-close log points toward lifecycle ordering; a browser-disconnect or process-exit log points toward browser health or external shutdown.
4. Handle Promise.race and competing waits
A common trap is racing navigation or a page event against a timeout. Promise.race() returns when the first promise settles; it does not cancel the other promises. A losing wait can remain active while the code closes the page. Later, that wait may reject because its target has disappeared.
Keep references to the operations and settle the ones still pending before closing the page. For a simple race where both operations are expected to settle quickly, this pattern waits for the loser after the winner:
const navigation = page.waitForNavigation({ waitUntil: 'domcontentloaded' });
const click = page.click('a.next');
try {
await Promise.all([navigation, click]);
} catch (error) {
// Do not start page cleanup until page work has settled.
await Promise.allSettled([navigation, click]);
throw error;
}
When you truly need a timeout race, retain every promise and use a cancellation mechanism for operations that support it. If an operation cannot be cancelled, await or otherwise handle its eventual settlement before closing the page. Attach rejection handling as soon as promises are created; do not leave a losing promise unobserved. Check that timeout callbacks cannot close a page after the screenshot has already started.
Prefer one clear owner for page cleanup. If a timeout handler, request handler, and finally block can all close the same page, make the cleanup idempotent or coordinate them so one owner performs it after outstanding work ends.
5. Await evaluate work and callbacks
Page evaluations, exposed functions, and callbacks can also outlive the code that started them. If cleanup runs immediately after page.evaluate(), check what that evaluation returns and whether it starts asynchronous work that the returned promise does not represent. Await the promise that represents the work you need, and make sure callbacks do not try to use the page after it has closed.
const result = await page.evaluate(async () => {
// Return or await the asynchronous work needed by the caller.
const response = await fetch('/data');
return response.status;
});
console.log('Page operation completed:', result);
await page.screenshot({ path: 'shot.png' });
A delay after evaluation is not a reliable substitute for awaiting work. A reported Puppeteer issue found that delays did not resolve a page close/reopen failure; the useful investigation is the cleanup and evaluation ordering.
6. Diagnose Chromium crashes and container failures
If the error occurs during launch, appears randomly in Docker, or arrives with a browser disconnect, look beyond page-level cleanup. Capture Chromium stderr and the process exit code. Check that the runtime image has the shared libraries Chromium needs. A Puppeteer issue, for example, reported a missing libgobject-2.0.so.0 library in an error that surfaced as a target-closed protocol message.
For Docker or Alpine, compare the Node.js version, Puppeteer version, Chromium version, and base image. Confirm that the installed browser matches the way Puppeteer is configured to launch it. While isolating the problem, remove experimental launch flags and reduce the setup to one page and one screenshot. This helps distinguish a runtime failure from application concurrency.
Do not add flags or increase delays at random. Change one environment variable at a time and keep the failing stderr, exit status, and exact launch configuration. Some reports describe intermittent Target.setAutoAttach: Target closed in container setups; that is a different command from screenshot capture, but the same process-health checks apply.
7. Record versions before comparing fixes
Error classes, stack traces, and target/session behavior can vary between Puppeteer releases. Record enough information to reproduce the report:
- Puppeteer package version and whether it is
puppeteerorpuppeteer-core. - Node.js version.
- Chromium or Chrome version and how it was installed.
- Operating system, container base image, and architecture.
- The command in the error, whether failure occurs during launch or capture, and relevant stderr.
Use the version installed in the failing environment when consulting the changelog and issue discussions. A fix described for an older target/session error may not apply unchanged to a current release.
8. Troubleshooting checklist
| Symptom | Likely cause | What to do |
|---|---|---|
Failure at page.screenshot(), then a close log |
Page closed before screenshot completed. | Await the screenshot and move cleanup after it. |
| Failure after a timeout or a winning race | A losing wait remains active or a timeout closes the page. | Keep references, cancel supported work, and settle remaining operations before cleanup. |
| Failure after evaluation or exposed function work | Async work or callback outlives the page. | Return and await the required work; order callbacks and cleanup. |
| Browser disconnects or exits | Chromium crashed, was killed, or failed to start. | Inspect stderr, exit code, libraries, and launch configuration. |
| Only happens in a container | Runtime, browser, or base-image compatibility issue. | Compare versions and libraries; simplify flags and reproduce in a minimal container. |
| Old fix does not match the stack trace | Puppeteer version changed error behavior. | Record exact versions and consult relevant release history. |
9. Performance, reliability, and cost considerations
Waiting strategy affects both capture time and failure exposure. networkidle2 can be useful for pages that finish loading their important resources, but pages with long-lived requests may not become idle as expected. domcontentloaded can be quicker when a screenshot does not depend on late content. If a page needs a particular component, waiting for a selector can be more targeted than waiting for all network activity. Choose the condition that matches the content you need, and use a bounded timeout with explicit failure handling.
Full-page screenshots can involve more work and memory than viewport captures, especially on long pages. Capture only the area you need when possible, and avoid running more browser jobs concurrently than the host can sustain. When an operation fails, close resources in a controlled order and report the failed capture; do not blindly retry forever. For retries, use a limit and retain the original error and browser diagnostics so a persistent runtime problem does not become an endless loop.
Self-hosted Puppeteer costs include the compute and maintenance of the browser runtime, plus the engineering time to keep Chromium compatible with the deployment image. For a one-off or low-volume capture, local automation may be the simplest option. For production capture workloads, account for concurrency, memory, browser restarts, and investigation time when comparing that setup with a screenshot API.
10. Or skip the browser setup
If your goal is a screenshot rather than managing Chromium, ScreenshotNeo provides a website screenshot API and an MCP server. Its one-call API returns an image or PDF; the request below saves a WebP response. See the ScreenshotNeo documentation for API options.
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', image);
Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month, with no card.
11. Frequently asked questions
Does “Target closed” mean Puppeteer closed my page?
Not necessarily. Your code may have closed the page, but the browser could also have disconnected or Chromium could have crashed. Use close and disconnect logs plus process stderr to distinguish them.
Will adding a delay fix the error?
Usually it only changes timing. Await the operation that matters and identify the code path that closes the target. A delay cannot restore a closed page or repair a missing browser library.
Should I retry the screenshot automatically?
Only after deciding which failures are transient and limiting retries. Preserve the first error and diagnostics; repeated retries will not fix a missing library or a deterministic premature close.
Why does the error name a different CDP command?
The target can disappear while any protocol operation is pending. The command in the message tells you what was in flight; it does not by itself identify what closed the target.
Primary references
- Puppeteer issue #1385, reporting
Page.captureScreenshot: Target closed. - Puppeteer issue #6610, discussing awaited operations and outstanding promises in a race.
- Puppeteer issue #6258, showing a missing shared library surfacing during launch.
- Puppeteer issue #10153, reporting intermittent target closure in a container environment.
- Puppeteer issue #1385 discussion, documenting that delays did not resolve one close/reopen report.
- Puppeteer changelog, for release-specific changes to target and session errors.


