How to Get a JavaScript Handle from a Web Worker in Puppeteer
Use Puppeteer’s WebWorker.evaluateHandle() to keep a reference to a worker-side value, then read or dispose of it in the worker’s context.
Use Puppeteer’s WebWorker.evaluateHandle() to evaluate a function inside a worker and receive a JSHandle for its result. Use jsonValue() when you need a serializable copy in Node.js, and call dispose() when you no longer need the handle.
const worker = await new Promise(resolve => page.once('worker', resolve));
const resultHandle = await worker.evaluateHandle(() => ({ answer: 42 }));
try {
console.log(await resultHandle.jsonValue());
} finally {
await resultHandle.dispose();
}
The worker must be created after the listener is registered for this short pattern to catch it. In a real script, register the listener before the action that starts the worker, or check and track existing workers as shown below. evaluateHandle() returns a handle to the function’s result in the worker context; it does not return a handle to the worker itself. See Puppeteer’s WebWorker API and evaluateHandle reference.
1. Install Puppeteer and capture a worker
This complete Node.js example starts a page that creates a dedicated worker, waits for the worker event, obtains a handle to a worker-side object, reads its serializable value, and cleans up. Save it as worker-handle.js.
npm install puppeteer
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
let handle;
try {
const page = await browser.newPage();
// Attach before navigation, because navigation creates the worker.
const workerCreated = new Promise(resolve => {
page.once('workercreated', resolve);
});
await page.goto('data:text/html,<title>Worker demo</title>');
await page.evaluate(() => {
const source = `self.demoValue = { answer: 42, status: 'ready' };`;
const blob = new Blob([source], { type: 'text/javascript' });
const workerUrl = URL.createObjectURL(blob);
new Worker(workerUrl);
});
const worker = await workerCreated;
console.log('Worker URL:', worker.url());
handle = await worker.evaluateHandle(() => self.demoValue);
console.log('Worker value:', await handle.jsonValue());
} finally {
if (handle) await handle.dispose();
await browser.close();
}
})().catch(error => {
console.error(error);
process.exitCode = 1;
});
The example uses Puppeteer’s documented workercreated page event and page.workers() API to manage worker lifecycle. Event names and API details can vary by Puppeteer version, so check the reference for the installed version. This example uses a data URL only to keep the worker demo self-contained; for application code, navigate to your page and register the listener before the code that creates its worker.
2. Get a handle from the worker
Call worker.evaluateHandle(function, ...args). The function body runs in the worker’s global environment, not in the page’s DOM. You can return an object, an instance, or another value for which you need a remote reference.
const workerObject = await worker.evaluateHandle(() => {
return { status: 'ready', values: [1, 2, 3] };
});
try {
// Keep using workerObject for worker-side operations here.
} finally {
await workerObject.dispose();
}
Arguments passed after the function are supplied to that function in the worker. For example:
const recordHandle = await worker.evaluateHandle(id => ({ id, found: true }), 'job-17');
try {
console.log(await recordHandle.jsonValue());
} finally {
await recordHandle.dispose();
}
The function cannot close over variables from your Node.js scope: it is serialized and evaluated in the browser worker. Pass values as arguments instead. Do not access document or page-only globals from a worker function; a worker has its own global environment.
3. Choose between evaluate and evaluateHandle
| Method | Use it when | What you get |
|---|---|---|
worker.evaluate() |
You need a plain, serializable result in Node.js. | A deserialized value. Complex objects may be truncated or become an empty object. |
worker.evaluateHandle() |
You need a remote reference, need to work with a mutable object, or ordinary serialization does not preserve the value adequately. | A JSHandle associated with the worker’s execution context. |
For example, this is simpler if the result is plain data:
const value = await worker.evaluate(() => ({ answer: 42 }));
console.log(value.answer);
Use a handle for a worker-side object that you intend to keep referencing. A handle is not a general-purpose copy: when you want plain data in Node.js, request it with jsonValue(). Puppeteer’s evaluate documentation explains the deserialization behavior and recommends handles when serialization is inadequate or a mutable reference is needed.
4. Find the right worker reliably
Pages can create multiple workers, and a worker may already exist by the time your code starts listening. Filter by worker.url() and inspect current workers as well as future events. The following helper resolves an existing matching worker immediately or waits for a matching worker to be created:
function findOrWaitForWorker(page, matchesUrl, timeoutMs = 10000) {
const existing = page.workers().find(worker => matchesUrl(worker.url()));
if (existing) return Promise.resolve(existing);
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
page.off('workercreated', onCreated);
reject(new Error(`No matching worker appeared within ${timeoutMs} ms`));
}, timeoutMs);
function onCreated(worker) {
if (!matchesUrl(worker.url())) return;
clearTimeout(timer);
page.off('workercreated', onCreated);
resolve(worker);
}
page.on('workercreated', onCreated);
});
}
const workerPromise = findOrWaitForWorker(
page,
url => url.includes('/worker.js')
);
// Trigger the app action that creates the worker after registering the listener.
await page.click('#start-worker');
const worker = await workerPromise;
const handle = await worker.evaluateHandle(() => self.config);
try {
console.log(await handle.jsonValue());
} finally {
await handle.dispose();
}
For robust application code, use a URL or another stable property to distinguish workers, set a finite timeout, and remove listeners when the wait ends. A worker can be destroyed or replaced during navigation; reacquire it after the page creates a new one.
5. Handle lifetime and execution contexts
- A handle belongs to the worker execution context that created it. Keep operations on that handle in its owning worker context.
- Do not assume a live handle can be passed to page evaluation or to a different worker. If another context needs the information, extract a plain value with
jsonValue()and pass that value. - Dispose handles in a
finallyblock so errors do not leave remote references hanging around. - Puppeteer also disposes handles when their associated context is destroyed. Explicit disposal releases references promptly when your work finishes sooner.
- Do not hold handles to large or numerous objects longer than needed. They keep remote objects available and can add memory pressure.
6. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| The worker wait never resolves. | The listener was attached after worker creation, the page did not create the worker, or the URL filter is wrong. | Register before the triggering action; check page.workers() and log worker.url(); add a timeout. |
worker.evaluateHandle is not a function. |
The value is not a Puppeteer WebWorker, or the installed version/API usage differs. |
Obtain the object from the page’s worker event or worker list, and consult the API reference for your installed Puppeteer version. |
document is not defined. |
The evaluation runs inside a worker, which does not provide the page’s DOM global. | Evaluate worker globals or data. Use page.evaluate() for page DOM work. |
| The returned value is empty, incomplete, or hard to use. | You used evaluate() for a value that does not serialize cleanly. |
Use evaluateHandle(), then access the remote object through the handle or obtain a deliberate serializable representation. |
| A handle operation fails after navigation or worker shutdown. | The owning execution context was destroyed with its worker. | Wait for the replacement worker and create a new handle; do not reuse a handle from the old context. |
| Node.js variables are undefined inside the callback. | Evaluation callbacks run in the browser context and cannot capture Node.js lexical variables. | Pass the values as arguments: worker.evaluateHandle((input) => ..., input). |
7. Performance, reliability, and cost
Worker evaluation crosses the automation protocol boundary. Keep returned values small when copying them into Node.js, and avoid repeated round trips when one worker-side evaluation can do the work. A handle avoids immediately serializing the whole object, but it retains a remote reference until disposal or context destruction, so it is not a free substitute for copying data.
For reliability, attach lifecycle listeners before triggering work, identify the intended worker, impose a timeout, and handle worker replacement. Always dispose handles in cleanup paths. Puppeteer itself is the browser automation dependency; actual runtime and resource use depend on the page, browser, and workload. The API has no per-handle price specified in the cited documentation.
Or skip the browser setup
If your goal is a website screenshot rather than a worker-side JavaScript reference, ScreenshotNeo provides a screenshot API and MCP server. It does not return Puppeteer worker handles. Its screenshot flow removes cookie banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are never billed. AI agents can capture through its MCP server. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. See the API documentation.
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}`);
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
Frequently asked questions
Does the handle represent the worker itself?
No. The worker is the WebWorker object. The handle represents the value returned by a function evaluated in that worker.
Can I get a handle to a function in the worker?
Yes. Return the function from evaluateHandle() to obtain a remote handle, and use it while its worker context remains alive.
Can I reuse a handle after creating another worker?
No. Treat each handle as owned by the context that created it. Obtain a new handle from the new worker.
When should I use jsonValue()?
Use it when you need a serializable representation in Node.js. It is not a replacement for the remote handle when you need to keep working with the original worker-side object.


