ScreenshotNeo

BlogHow-to

How to Wait for a Function in a Web Worker with Puppeteer

Use Puppeteer’s WebWorker.waitForFunction to wait for worker state. Learn how to find the right worker, handle timeouts, and troubleshoot context errors.

By the ScreenshotNeo team4 October 20267 min read

To wait for state inside a Web Worker with Puppeteer, call worker.waitForFunction(). Its predicate runs in that worker’s execution context. page.waitForFunction() runs in the page context, so it cannot directly read worker globals.

The examples below use Puppeteer’s worker lifecycle event and API. They assume your worker exposes a condition such as self.ready; replace the example URL and predicate with values from your application. See the official WebWorker.waitForFunction reference and WebWorker API for the current signatures.

1. Wait for a worker that will be created

Register the workercreated listener before triggering the application action. Otherwise, a quickly created worker could appear before the listener is attached.

const workerReady = new Promise(resolve => {
  function onWorkerCreated(worker) {
    if (worker.url().includes('/worker.js')) {
      page.off('workercreated', onWorkerCreated);
      resolve(worker);
    }
  }

  page.on('workercreated', onWorkerCreated);
});

await page.evaluate(() => {
  // Replace with the action that creates your worker.
  document.querySelector('#start').click();
});

const worker = await workerReady;
await worker.waitForFunction(() => self.ready === true, {
  timeout: 10_000,
});

This pattern has two waits: first for the browser to create the matching worker, then for a condition inside it. The URL filter matters when a page creates more than one worker. Choose a stable, application-specific match rather than accepting the first worker indiscriminately.

2. Wait on a worker that already exists

If navigation or setup has already created the worker, inspect page.workers() instead of waiting for a future creation event.

const worker = page.workers().find(worker =>
  worker.url().includes('/worker.js')
);

if (!worker) {
  throw new Error('Expected worker was not found');
}

await worker.waitForFunction(() => self.ready === true, {
  timeout: 10_000,
});

page.workers() lists dedicated WebWorkers associated with the page; it does not include ServiceWorkers. Puppeteer also emits workerdestroyed when a worker ends. See the official Page.workers reference for details.

3. Choose the wait that matches the state

What you are waiting for Use Context or object checked
A variable or computation state inside a dedicated worker worker.waitForFunction() The selected worker’s execution context
A DOM change or value on window page.waitForFunction() The page’s execution context
A new browser target, such as a popup page browserContext.waitForTarget() A target matching the supplied predicate
A result the application has sent back to the page page.waitForFunction() or a locator wait The page-visible result

Page and worker contexts are separate. A page predicate that reads self.ready reads the page’s global scope, not the worker’s. If the application posts a result message and renders it into the DOM, waiting for that rendered result is valid, but it observes the page-side outcome rather than the worker’s internal state. A target wait is for matching browser targets, not for state inside an existing worker. See Page.waitForFunction and the BrowserContext.waitForTarget reference.

4. Configure polling, timeout, and cancellation

WebWorker.waitForFunction(workerFunction, options, ...args) waits until the worker-side function returns a truthy value. Its documented options include polling, timeout, and signal. Set a timeout that fits the expected operation and handle rejection in the calling code. Check the current API reference for supported polling values and option details for your installed Puppeteer version.

const controller = new AbortController();
const timeoutMs = 15_000;

try {
  const resultHandle = await worker.waitForFunction(
    (expected) => self.status === expected,
    {
      timeout: timeoutMs,
      polling: 100,
      signal: controller.signal,
    },
    'complete'
  );

  // A wait handle is returned. For a boolean readiness check, the
  // synchronization itself is usually what the caller needs.
  await resultHandle.dispose();
} catch (error) {
  console.error('Worker did not reach the expected state:', error);
  throw error;
}

The callback can receive serializable arguments after the options argument, as shown. The returned promise resolves to a handle for the result type. Dispose of a handle when you do not need it, especially in repeated automation. An AbortSignal is useful when a surrounding operation is cancelled; timeout remains useful for bounding a wait that never reaches its condition.

The example uses a 100 ms polling interval to make the option concrete, not as a universal performance recommendation. Choose polling and timeout based on how quickly the state can change and how much delay your workflow can tolerate.

5. Make the worker expose a waitable condition

The predicate must inspect something available in the worker global scope. In a browser worker, that global is self. For example, application worker code might set a status after initialization:

// worker.js (illustrative application code)
self.status = 'starting';

async function initialize() {
  // Perform worker setup or computation.
  self.status = 'complete';
}

initialize();

Puppeteer can then wait with () => self.status === 'complete'. If the worker does not expose state, use a condition that it does expose, or wait for a result the page receives through the application’s normal message flow. A function passed to Puppeteer is evaluated in the worker context; it cannot close over ordinary Node.js variables. Pass values through the supported arguments instead.

6. Handle races and worker lifecycle changes

  • Attach before the trigger: create the worker event promise before clicking, navigating, or evaluating code that starts the worker.
  • Filter deliberately: match the worker URL or another stable identifier when several workers can be created.
  • Account for existing workers: use page.workers() if creation may have happened already. Do not rely on a future event to report an existing worker.
  • Bound both stages: give worker discovery and the in-worker condition appropriate failure handling so neither can wait forever.
  • Expect teardown: navigation or application behavior can destroy a worker. A wait against a worker that terminates cannot observe future state; recreate or reacquire the worker after the lifecycle transition.
  • Know the scope: page.workers() is not a ServiceWorker enumeration API.

A robust flow treats worker creation, worker readiness, and page-visible completion as different milestones. Wait for the milestone your next step actually requires.

7. Troubleshooting

Symptom Likely cause Fix
The wait times out even though the page looks ready The predicate is running in the worker, but the condition exists only in the page, or the worker never sets the expected state. Inspect the worker code and choose a worker-global condition. If the result is only reflected in the page, use a page-side wait instead.
No worker is found The listener was attached after creation, the URL filter does not match, or the worker is not a page’s dedicated WebWorker. Attach before the trigger; inspect page.workers() and worker URLs; confirm whether the app uses a ServiceWorker or another target type.
The wrong worker is selected The page has multiple workers and the filter is too broad. Use a more specific URL or application-owned distinction, and validate the selected worker before waiting.
Predicate throws or cannot see a Node variable The function executes in the browser worker context and does not share Node.js lexical scope. Use worker globals and pass serializable values via the function arguments.
The worker disappears during the wait The application terminated it or navigation replaced it. Wait for the appropriate lifecycle event, then reacquire the worker after the action that creates its replacement.
Cancellation or timeout is handled as success The rejection from the wait is swallowed or not propagated by surrounding code. Catch the failure at the boundary, report the operation and worker URL, then fail or retry according to the application’s policy.

8. Performance, reliability, and cost

This wait runs a predicate in the worker context until it becomes truthy, so keep the predicate small and free of side effects. Polling more often can reduce detection delay while increasing evaluation work; a longer interval reduces checks but may add latency. Use the smallest practical timeout that still covers normal startup variability, and make timeout failures visible in logs with the worker URL and operation being awaited.

For reliability, register lifecycle listeners before triggering creation, distinguish creation from readiness, and account for worker teardown. There is no universal timeout or polling interval for all applications; choose values from your workload’s expected behavior. Puppeteer itself is the browser automation mechanism here, so direct execution has the costs of running and maintaining the browser environment in your own infrastructure.

9. Or skip the browser setup

If your actual goal is to capture a website screenshot rather than inspect a worker’s internal state, ScreenshotNeo provides a website screenshot API and MCP server for developers. It does not replace Puppeteer’s worker-state wait; it avoids browser setup for the screenshot task.

One GET request returns an image or PDF. This runnable cURL example saves a WebP screenshot:

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 options. Equivalent Python and Node.js calls:

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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
  • Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, with response headers indicating the page verdict and billing status.
  • An MCP server gives AI agents tools for screenshots, page information, and PDF capture.
  • 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000. Every feature is available on every plan.

Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.

10. FAQ

Can I wait for a worker function to return a specific value?

Yes. Make the predicate return a truthy result when the desired condition is met, and pass expected values as arguments when useful. The promise resolves with a handle for the awaited result.

Does this method work for a ServiceWorker?

The page’s workers() method covers dedicated WebWorkers and excludes ServiceWorkers. Use APIs and target handling appropriate to the ServiceWorker lifecycle rather than assuming it appears in that list.

Should I wait for worker readiness or for the page result?

Wait for worker readiness when the next step depends on the worker’s own state. Wait for the page result when the application communicates completion back to the DOM or page global.

Where can I check the current Puppeteer option details?

Use the official WebWorker.waitForFunction API reference; option behavior may change between Puppeteer versions.