Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
“Target closed” means Puppeteer lost the DevTools target behind a page—or the browser connection itself—while an operation such as navigation, evaluation, waiting, or screenshot capture was still running. For multi-URL jobs, the dependable baseline is one long-lived browser, a small bounded worker pool, one page (or isolated BrowserContext) per job, an await for every protocol call, and cleanup in finally blocks. Close a page only after its screenshot promise settles, and close the browser only after every job has settled.
What the error actually means
Puppeteer talks to Chromium through the Chrome DevTools Protocol. A page is represented by a target. If that target is destroyed, or Chromium disconnects, the next pending protocol command rejects with an error commonly written as Target closed, TargetCloseError, or a related protocol exception.
The message describes a lifecycle failure, not a bad URL by itself. The failing command might be goto(), evaluate(), waitForSelector(), or screenshot(). In a batch, a race in one job can also expose a browser-wide crash that invalidates every other page.
Recommended Free Tools
The safe lifecycle for a screenshot batch
One browser, bounded pages
A single Browser can own many Page instances, each with its own viewport. Reusing that browser avoids the startup cost and failure surface of launching Chromium for every URL. Do not turn the URL list into an unbounded Promise.all(): dozens of simultaneous pages and screenshots can exhaust memory, CPU, file descriptors, or the host’s process limit.
#1 Best Overall
Start with a low worker count (for example, two), observe memory, CPU, browser-disconnect events, and output latency, then increase it gradually. The right number depends on the pages and machine; it is not a universal Puppeteer setting.
One page or one BrowserContext per job
A fresh page per capture gives each URL a clear lifecycle. Use a BrowserContext when cookies, local storage, or session state must be isolated between jobs; closing that context closes all pages inside it. Reusing one page is possible, but every navigation, wait, evaluation, and screenshot must be serialized and any page state must be deliberately reset.
Await every protocol operation
Never start a screenshot, evaluation, or navigation and then close the page, context, or browser. During a screenshot, Puppeteer waits for BrowserContext.newPage(), Browser.newPage(), and Page.close() to finish the screenshot. Page.bringToFront() does not wait for an existing screenshot, so it is not a completion signal.
Free tools Windows power users keep installed
One-click scans. No signup required.
A production-safe Puppeteer implementation
The following CommonJS-compatible example launches one browser, captures jobs through a bounded pool, records lifecycle events, and always closes resources. Set dumpio: true while diagnosing a browser crash.
Rank #2
const puppeteer = require('puppeteer');
async function capture(browser, url, path) {
const page = await browser.newPage();
page.on('error', err => console.error('page error', url, err));
page.on('close', () => console.error('page closed', url));
page.on('console', msg => console.log('[page]', url, msg.type(), msg.text()));
try {
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto(url, { waitUntil: 'networkidle2', timeout: 45000 });
await page.screenshot({ path, fullPage: true });
return { url, path, ok: true };
} finally {
if (!page.isClosed()) await page.close();
}
}
async function runWithConcurrency(items, limit, worker) {
const results = new Array(items.length);
let next = 0;
async function runner() {
while (true) {
const index = next++;
if (index >= items.length) return;
try {
results[index] = await worker(items[index], index);
} catch (error) {
results[index] = { ...items[index], ok: false, error: error.message };
}
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, runner));
return results;
}
(async () => {
const urls = [
{ url: 'https://example.com/', path: 'example.webp' },
{ url: 'https://example.org/', path: 'example-org.webp' }
];
const browser = await puppeteer.launch({ headless: true, dumpio: false });
browser.on('disconnected', () => console.error('browser disconnected'));
browser.on('targetdestroyed', target => console.error('target destroyed', target.url()));
try {
const results = await runWithConcurrency(urls, 2,
(job) => capture(browser, job.url, job.path));
console.log(results);
} finally {
await browser.close();
}
})();
The finally in capture runs after goto and screenshot settle, including when either throws. The outer finally prevents the browser from being left behind after a batch failure. If the browser has already disconnected, do not assume a destroyed page can be reused; restart the browser and retry the whole job.
Choosing an isolation strategy
| Approach | Lifecycle safety | Isolation | Resource use | Failure scope |
|---|---|---|---|---|
| One shared page | Safe only with strict serialization and complete state reset | Cookies and storage can leak between URLs | Lowest page count | Usually one job, but a page error can disrupt the queue |
| New page per job | Clear close point after the screenshot | Separate DOM; browser profile state is shared | Moderate | Usually one page |
| New BrowserContext plus page | Explicit context close after all page work | Cookies and local storage isolated | Higher than a page alone | Context jobs are isolated; browser crash affects all |
| New browser per URL | Simple boundaries, but expensive | Strongest process boundary | Highest CPU, memory, and startup cost | Usually one URL, unless the host is exhausted |
Why multi-URL captures fail
Closing a page too early
This is the most direct race: code calls page.close() while goto, evaluate, waitForSelector, or screenshot is unresolved. Move cleanup into an awaited finally and never fire-and-forget a protocol call.
Closing or crashing the browser
browser.close() destroys every target. Chromium can also exit because of a crash, missing shared libraries, sandbox restrictions, an unwritable profile, or process limits. Puppeteer emits disconnected when the browser closes or crashes and targetdestroyed when a target is closed.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallClose-and-reopen races
A long-running page.evaluate() can still be pending when code closes the page and immediately calls browser.newPage(). Reports of TargetCloseError in this pattern show why arbitrary sleeps are not a fix. Await the old operation, await the close, verify browser health, and only then create the next page.
Unbounded parallelism
Promise.all(urls.map(capture)) starts every capture at once. Large pages, full-page screenshots, fonts, video, and JavaScript-heavy applications can make Chromium exceed available memory. A worker pool limits simultaneous work while preserving throughput.
Debugging checklist
- Identify what disappeared. Add
browser.on('disconnected'),browser.on('targetdestroyed'), and pageclose/errorlisteners. A page close points to lifecycle code; a browser disconnect points to Chromium or the host. - Make the browser visible. Re-run with
headless: falseor a nonzeroslowMovalue to watch navigation and teardown. - Capture page logs. Forward
page.on('console')andpage.on('pageerror')output so application failures are not mistaken for Puppeteer failures. - Inspect protocol traffic. Set the environment variable
NODE_DEBUG=puppeteer:*. Inspectbrowser.debugInfo.pendingProtocolErrorsfor unresolved calls when available in your Puppeteer version. - Expose Chromium stderr. Launch with
dumpio: true; the browser process may report a sandbox, library, GPU, or crash problem there. - Check the host. Verify sandbox permissions, required system libraries, writable profile and temporary directories, container process limits, available memory, and zombie Chrome processes.
- Retry at the right level. If only a page target died, retry that URL with a new page. If
disconnectedfired, restart Chromium and retry the complete job; do not keep issuing commands to the dead browser.
Navigation, waiting, and screenshot settings that reduce surprises
Navigation completion
waitUntil: 'networkidle2' is a useful baseline for pages that load assets after the initial HTML, but analytics, chat, streams, or long polling can prevent an idle state. Use a finite timeout and, where appropriate, wait for a specific selector instead of waiting forever. A timeout should be handled as a failed job and still pass through cleanup.
Page readiness
For lazy-loaded content, scroll or wait for the element that must appear before calling screenshot. Keep that work inside the same awaited capture function. Do not close a page from a timeout callback while another operation is running.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Full-page resource cost
Full-page screenshots require more layout and memory than a viewport shot, especially on very tall documents. If the output does not need the entire document, capture a bounded viewport or a specific element. This reduces pressure on the browser and makes worker-count tuning easier.
Rank #4
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF, so your batch code does not need to manage Chromium pages, contexts, or browser shutdown. It accepts cookie and consent banners before capture, then removes more than 60 known consent platforms along with newsletter popups and chat widgets; each cleanup step can be disabled.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for parameters and response handling. The same endpoint supports full-page shots with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets or any viewport, retina scale, custom CSS and JavaScript, clicks before capture, hidden selectors, waits for a selector/delay/network idle, blocked ads/trackers/requests/resource types, custom headers/cookies/user agents/Authorization, timezone and geolocation, transparent backgrounds, resizing, chosen-TTL caching, signed public-image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Parameter names used by other screenshot APIs also work, which can simplify migration.
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}`);
Only clean shots are billed. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and each response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients, allowing an AI agent to capture pages without custom browser orchestration.
| Plan | Included shots | Price |
|---|---|---|
| Free | 1,000/month | $0, no card |
| Starter | 3,000 | $5 |
| Growth | 15,000 | $15 |
| Pro | 60,000 | $39 |
| Scale | 250,000 | $99 |
| Business | 1,000,000 | $249 |
Yearly billing provides two months free, and every feature is included on every plan. Create a free ScreenshotNeo account to get 1,000 screenshots a month without a card.
FAQ
Can page.bringToFront() prove that a screenshot is finished?
No. It does not wait for an existing screenshot. Await the screenshot promise itself.
Best Value
Should I add a fixed delay before closing a page?
No. A delay can hide a race without synchronizing the pending protocol operation. Await the operation and then close in finally.
What should I do if one URL repeatedly crashes Chromium?
Run that URL alone with browser stderr enabled, reduce concurrency, and inspect its memory-heavy resources or scripts. If the browser disconnects, restart it before retrying.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Fix the driver behind crashes, sound loss and screen glitches3Repair Windows errors before they cause bigger problemsFrequently Asked Questions
Does a new page guarantee independent cookies?
No. Pages in the same browser profile share browser-level state. Use a separate BrowserContext when cookie and local-storage isolation is required.
Is networkidle2 always the best wait condition?
No. Applications with long polling or streaming may never become idle; a finite timeout plus a page-specific readiness selector can be more predictable.
Can I safely reuse a page after a Target closed error?
Only if the browser remains connected and the page is still open. After a target or browser disconnect, create a new page or restart the browser rather than issuing commands to the destroyed target.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.

