How to Run JavaScript in a Web Worker with Puppeteer
Use Puppeteer’s WebWorker API to run JavaScript in a dedicated worker. Learn how to find workers, pass data, await results, handle timing, and troubleshoot common issues.
Use Puppeteer’s WebWorker object to run JavaScript in a dedicated Web Worker. Listen for the page’s workercreated event before the action that starts the worker, then call worker.evaluate(). page.evaluate() runs in the page’s main JavaScript context; it does not run inside the worker.
This guide uses Puppeteer’s documented worker lifecycle and evaluation APIs. Check the API reference and your installed Puppeteer version for the exact signatures available in your project: WebWorker, Page.workers(), and Page.evaluate().
1. Run code in a worker created during navigation
Install Puppeteer in a Node.js project, save this as worker-example.mjs, and run it with node worker-example.mjs. Replace the example URL with a page that creates a dedicated Web Worker during startup.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
// Subscribe before navigation so an early worker is not missed.
const workerCreated = new Promise(resolve => {
page.once('workercreated', resolve);
});
await page.goto('https://example.com');
const worker = await workerCreated;
console.log('Worker URL:', worker.url());
const result = await worker.evaluate(() => {
// This function executes in the worker context.
return self.location.href;
});
console.log('Result:', result);
} finally {
await browser.close();
}
The promise is created before navigation, but awaited after it. This matters because a page can create its worker as soon as its scripts run. If the worker is created only after a button click, register the event listener first, click the button, and then await the promise.
2. Select an existing worker or handle multiple workers
If the worker is already running, use page.workers(). It returns the page’s active dedicated WebWorkers; it does not include ServiceWorkers. Use worker.url() to identify the intended worker rather than assuming the first entry is the right one.
const workers = page.workers();
const worker = workers.find(w => w.url().includes('/workers/calculate.js'));
if (!worker) {
throw new Error(`Target worker not found. Active workers: ${workers.map(w => w.url()).join(', ')}`);
}
const result = await worker.evaluate(() => self.location.href);
console.log(result);
When several workers can start around the same time, collect lifecycle events and select by a URL pattern or other application-specific signal:
const workers = [];
page.on('workercreated', worker => workers.push(worker));
// Perform the navigation or interaction that starts the workers here.
await page.goto('https://example.com');
const target = workers.find(w => w.url().endsWith('/compute-worker.js'));
if (!target) throw new Error('compute-worker.js was not created');
Subscribe before the action. If you only inspect page.workers() after a short-lived worker has been created and destroyed, it may no longer be present. The page API also emits workerdestroyed, which is useful when tracking a worker’s lifecycle. See the official WebWorker reference.
3. Pass values into worker code and return results
Puppeteer serializes the function passed to evaluate() and runs it in the target browser context. It does not carry over Node.js variables or helper functions from the surrounding file. Pass values as arguments and define the logic inside the callback.
const factor = 7;
const result = await worker.evaluate(value => value * 6, factor);
console.log(result); // 42
The worker can also return JSON-like data:
const summary = await worker.evaluate(() => ({
href: self.location.href,
hasDocument: typeof document !== 'undefined',
hasWindow: typeof window !== 'undefined'
}));
console.log(summary);
Return primitives or plain serializable objects when possible. Browser protocol serialization does not preserve arbitrary object identity and complex objects may become empty or truncated. If you need to retain an in-context reference, use the worker’s evaluateHandle() and consult the installed version’s WebWorker API.
4. Await promises and wait for later worker state
If the callback returns a promise, worker.evaluate() waits for it to settle and returns its resolved value. For state that changes after the evaluation completes, use worker.waitForFunction().
await worker.evaluate(() => {
self.answer = 42;
});
await worker.waitForFunction(() => self.answer === 42, {
timeout: 5_000
});
const answer = await worker.evaluate(() => self.answer);
console.log(answer); // 42
waitForFunction() supports polling, timeout, and abort-signal options in the documented API. Use a finite timeout that matches the operation and handle timeout errors at the call site. See WebWorker.waitForFunction() and WebWorker.evaluate().
5. Run worker code in response to a page interaction
When a click starts the worker, create the event promise first. The following is a complete pattern; replace the selector and URL with those used by the application.
const workerCreated = new Promise(resolve => {
page.once('workercreated', resolve);
});
await page.click('#start-computation');
const worker = await workerCreated;
const result = await worker.evaluate(input => {
return input * input;
}, 12);
console.log(result); // 144
If the click may fail to create a worker, add an application-specific check and a bounded timeout around the wait. Do not register the listener after the click: a quickly created worker could emit the event before Puppeteer begins listening.
6. Know which JavaScript context you are using
| Call | Execution context | Use it for |
|---|---|---|
page.evaluate(fn) |
Page’s main JavaScript context | DOM and page globals |
worker.evaluate(fn) |
Selected dedicated Web Worker | Worker globals and computation |
page.workers() |
Returns active dedicated workers | Finding workers already running |
page.evaluateOnNewDocument(fn) |
New page document before its scripts run | Document initialization; not worker evaluation |
A dedicated worker generally does not have the page’s DOM. Worker code commonly uses worker globals such as self; do not assume document or window is available. Also, page.workers() is not a way to select a ServiceWorker. For the page/worker distinction, see Puppeteer’s page evaluation, WebWorker, and evaluateOnNewDocument() references.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The worker-created promise never resolves | The page did not create a dedicated worker, the trigger was not performed, or the listener was registered too late. | Register before navigation or interaction. Confirm the page actually starts a dedicated worker and add a bounded timeout to the wait. |
| The wrong worker receives the evaluation | The page has multiple workers and code selected the first one. | Match worker.url() against the expected script URL, and report all active worker URLs when selection fails. |
document or window is undefined |
The callback is running in a worker, which is not the page’s DOM context. | Use worker-supported globals and pass data from Node or the page explicitly. Use page.evaluate() only for page-context work. |
| A Node.js variable is undefined inside the callback | The callback is serialized and evaluated in Chromium; Node’s lexical scope is not captured. | Pass the value as an evaluate argument and put required helper logic in the callback. |
| The result is empty, incomplete, or not the expected object | The returned value is not represented faithfully by protocol serialization. | Return a primitive or plain object, or use evaluateHandle() if an in-context reference is required. |
waitForFunction() times out |
The expected condition never became true, the wrong worker was selected, or the timeout is too short. | Check the condition and worker URL, inspect worker state with a simple evaluation, and choose a justified timeout or abort signal. |
| Worker disappears during evaluation | The page terminated or replaced the worker before the operation completed. | Track workerdestroyed, wait for the app’s creation trigger, and retry only if the operation is safe to repeat. |
8. Performance, reliability, and version notes
- Keep work in the worker. Return only the result needed by Node; repeatedly transferring large values adds serialization overhead.
- Wait on the actual condition. A worker-created event confirms creation, not that application initialization or a later computation has finished. Use a result promise or
waitForFunction()for readiness. - Bound waits. Use task-appropriate timeouts and clean up the browser in a
finallyblock so a failed wait does not leave Chromium running. - Handle lifecycle changes. Pages can destroy and recreate workers. Re-select a worker after navigation or restart instead of retaining a stale reference.
- Check your installed API. Puppeteer’s documentation pages surfaced for this topic display differing version labels, and one execution guide is labeled Next. Those labels do not establish the version installed in your project or when a method first appeared. Confirm the package types and matching API docs before relying on an option or signature.
For ordinary Puppeteer worker evaluation, cost is the browser execution and infrastructure your application runs; no per-evaluation Puppeteer API charge is established by the cited documentation. Infrastructure cost depends on where and how you run Chromium.
9. Or skip the browser setup
If your goal is to capture a page image rather than execute logic inside its worker, ScreenshotNeo provides a website screenshot API and MCP server. It does not replace Puppeteer’s worker evaluation: use it for the screenshot task.
For API parameters, see the ScreenshotNeo documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
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 bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
Cookie banners, popups, and chat widgets are removed before capture. Bot checks, blank pages, and failed loads are never billed; response headers indicate the page verdict and billing status. An MCP server lets AI agents use screenshot tools. The Free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000 shots.
Sign up free for 1,000 screenshots a month, with no card required.
10. FAQ
Can Puppeteer execute a function in a Web Worker?
Yes. Get the page’s dedicated WebWorker and call worker.evaluate() with the function.
Can I use page.evaluate() to access a worker?
page.evaluate() runs in the page context. Use worker.evaluate() for a selected worker context.
Does page.workers() include ServiceWorkers?
No. The documented list is for dedicated WebWorkers.
Can the callback use functions declared in my Node.js file?
Not through lexical scope. Pass input values as arguments and include the code the browser must run in the callback.
Does evaluateOnNewDocument() run code in a worker?
No. It concerns a new page document. Identify the worker and use its evaluation methods instead.


