Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use page.waitForFunction() when a custom condition concerns page-wide state, and locator.waitForFunction() when it belongs to a particular element. Both repeatedly evaluate a predicate until it returns a truthy value. They can receive an argument and a timeout; in the JavaScript API their documented default timeout is 0 (no timeout), so a finite timeout is usually safer in CI.

For ordinary UI readiness, prefer locator actions and web-first assertions. Playwright already auto-waits for actionability and retries assertions. A fixed page.waitForTimeout() delay should be limited to debugging because time-based tests are inherently flaky.

Choose the right wait

API Scope Best use Retry behavior
page.waitForFunction() Entire page A browser variable, document flag, computed value, or other global condition Evaluates the predicate in the page context until its result is truthy
locator.waitForFunction() One locator A custom condition attached to a specific element Re-resolves the locator on every retry, so it tolerates re-rendering
expect(locator).toHaveText() and other web-first assertions Element or page outcome A user-visible test result such as status text or visibility Retries until the assertion timeout and gives assertion-focused diagnostics
locator.waitFor() One locator Known state: attached, detached, visible, or hidden Waits for the selected locator state; visible is the default

Use a function wait only when the condition does not map cleanly to a locator state or assertion. Playwright describes locators as the central part of its auto-waiting and retry-ability, and most explicit waits are unnecessary before an action.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Wait for page-level state with page.waitForFunction()

The JavaScript and TypeScript signature is:

await page.waitForFunction(predicate, arg?, options?);

The predicate runs in the browser (page) context, not in Node.js. The call resolves when the return value is truthy. In the JavaScript API it returns a JSHandle, which you can read or dispose of when you need the value itself.

Basic example

import { test, expect } from '@playwright/test';

test('waits for a page flag', async ({ page }) => {
  await page.goto('https://example.com');

  await page.waitForFunction(() => window.innerWidth < 100);
});

A more realistic condition might be an application flag set after initialization:

await page.waitForFunction(() => window.__appReady === true);

Only use globals that the page actually exposes. A typo or an exception in the predicate causes the wait to fail rather than silently succeeding.

Pass an argument safely

The second parameter is serialized and supplied to the predicate in the page context. This keeps data out of interpolated JavaScript strings.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const selector = '.foo';

await page.waitForFunction(
  sel => Boolean(document.querySelector(sel)),
  selector
);

You can pass serializable strings, numbers, booleans, arrays, and objects. The predicate still executes in the browser, so Node-only objects and functions cannot be used directly as page values.

Use a finite timeout

await page.waitForFunction(
  () => window.__appReady === true,
  undefined,
  { timeout: 15_000 }
);

In JavaScript, both function-wait methods document timeout: 0 by default, meaning no timeout. That can leave a hung test indefinitely. Set a per-call timeout, or establish a project default:

page.setDefaultTimeout(15_000);
// Or for every page in a context:
browserContext.setDefaultTimeout(15_000);

A per-call option is explicit and easy to tune for a slow operation. A default protects waits you add later. Choose a value based on the operation and CI environment rather than hiding a race with an arbitrary large number.

Wait for an element with locator.waitForFunction()

Use the locator form when the predicate is about one element. It was added in Playwright v1.62.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const toggle = page.getByRole('button', { name: 'Menu' });

await toggle.click();
await toggle.waitForFunction(element =>
  element.hasAttribute('aria-expanded')
);

The locator is re-resolved on each retry. That matters in React, Vue, and other applications that replace DOM nodes during rendering: you do not wait on a stale, one-time element handle.

Pass an element-scoped argument

await page.getByTestId('status').waitForFunction(
  (element, value) => element.textContent === value,
  'Ready'
);

The first predicate parameter is the current element; the next parameter is your serialized argument. Keep the condition focused and deterministic. If the locator matches multiple elements, make it unique or narrow it first, so the wait expresses one intended target.

Prefer an assertion for visible outcomes

await expect(page.getByRole('status')).toHaveText('Ready');

This is clearer when the test requirement is “the user sees Ready.” Assertions retry automatically and report the expected and received values. Reserve locator.waitForFunction() for custom browser-side logic, such as checking a computed property or a DOM relationship that has no suitable assertion.

Predicates, promises, and cancellation

Truthy means success

The wait completes on any truthy result: true, a non-empty string, a non-zero number, or an object. Return an explicit boolean when possible; it makes the intent obvious.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.waitForFunction(() => {
  const progress = document.querySelector('#progress');
  return progress?.getAttribute('aria-valuenow') === '100';
});

Async predicates

If the predicate returns a Promise, Playwright waits for that Promise and then checks its resolved value.

await page.waitForFunction(async () => {
  const response = await fetch('/health');
  return response.ok;
});

Use this sparingly. Polling network endpoints from a page predicate can add load and obscure the real synchronization point; waiting for the application’s rendered state or a network event may be clearer.

Thrown and rejected predicates

If the predicate throws synchronously or its Promise rejects, the wait throws. Guard optional DOM access and make expected “not ready yet” states return false instead of throwing.

await page.waitForFunction(() => {
  const node = document.querySelector('[data-state]');
  return node ? node.getAttribute('data-state') === 'done' : false;
});

Abort a wait

Current APIs accept an AbortSignal. Aborting causes the operation to throw; it does not turn off the configured timeout.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const controller = new AbortController();
const wait = page.waitForFunction(
  () => window.__jobFinished === true,
  undefined,
  { timeout: 30_000, signal: controller.signal }
);

// Cancel from another branch when the job is no longer relevant:
controller.abort();
await wait;

Handle the resulting error when cancellation is an expected branch, and preserve the original error when it indicates a real test failure.

Why page.waitForTimeout() is flaky

A fixed delay guesses how long work will take. If the page is slower than the guess, the test races and fails; if it is faster, the test wastes time. A delay also says nothing about whether the desired state was reached. Playwright’s guidance is: “Never wait for timeout in production. Tests that wait for time are inherently flaky.”

Replace this:

await page.click('#save');
await page.waitForTimeout(1000);
expect(await page.locator('#status').textContent()).toBe('Saved');

with a state-based wait:

await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByRole('status')).toHaveText('Saved');

For a custom condition, use a function wait with a finite timeout instead of sleeping.

Known locator states and discouraged selector waits

When all you need is attachment or visibility, use:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.locator('#order-sent').waitFor({ state: 'visible' });

The supported states are attached, detached, visible, and hidden. For new code, prefer locator methods and assertions over page.waitForSelector(); the page-level selector API is discouraged because locator APIs provide better retry behavior and diagnostics.

Troubleshooting function waits

Timeout despite the condition appearing true

  • Confirm the predicate runs in the page context. Node variables are not automatically available there; pass serializable values as arg.
  • Check the exact type and value. A string such as 'false' is truthy, while the boolean false is not.
  • Verify the page, frame, and origin. The condition may exist in an iframe rather than the main document.
  • Capture diagnostics at failure: URL, console messages, and a screenshot or trace. A finite timeout turns an infinite hang into an actionable failure.

“Element is not defined” or stale-element behavior

Use a locator rather than an element handle, and use locator.waitForFunction() when the element can be replaced during rendering. The locator will be resolved again on each retry.

The predicate throws immediately

Protect missing nodes with optional chaining or an explicit null check. Return false for “not ready yet”; throw only for a condition that should fail the test immediately.

The wait never ends

Remember that the JavaScript default is no timeout. Set { timeout: ... } or a context/page default. If the condition is optional, use an abort signal or redesign the test so it waits for a bounded application event.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

An assertion is clearer

Replace a custom text or visibility predicate with a web-first assertion whenever possible. It communicates the user-visible contract and usually produces a better failure message.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance and reliability practices

  • Prefer a specific locator or page flag over scanning the whole DOM repeatedly.
  • Use the smallest predicate that answers the question; avoid expensive layout reads and repeated network requests.
  • Set realistic finite timeouts and keep the condition deterministic.
  • Synchronize on outcomes, not implementation timing. Actions already wait for actionability.
  • Use traces, screenshots, and console/network logging to diagnose a genuine race instead of adding a longer sleep.
  • Keep browser-side predicates serializable and independent of Node modules.

Or skip the browser setup

If your goal is to obtain a clean screenshot after a page reaches its state, ScreenshotNeo provides a website screenshot API and MCP server. It is separate from Playwright’s test synchronization, but can remove the browser-capture plumbing from scripts and AI workflows. A GET request returns PNG, JPEG, WebP, or PDF; options include waiting for a selector, a delay, or network idle, custom JavaScript, clicking before capture, full-page lazy-image loading, device presets, dark mode, and more.

cURL:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

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)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the ScreenshotNeo documentation for request options. Cookie and consent banners are accepted and 60-plus known consent platforms, newsletter popups, and chat widgets are removed before capture; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, with the result reported in 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. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Complete decision checklist

  1. Is the requirement a normal click, fill, navigation, visibility, or text outcome? Use a locator action or web-first assertion.
  2. Is it a known locator state? Use locator.waitFor().
  3. Is it custom page-wide browser logic? Use page.waitForFunction().
  4. Is it custom logic tied to a re-rendering element? Use locator.waitForFunction().
  5. Could the predicate hang? Supply a finite timeout and, when appropriate, an AbortSignal.
  6. Are you about to add a fixed sleep? Replace it with the state or predicate that proves readiness.

Frequently Asked Questions

What does Playwright return from waitForFunction?

The JavaScript API returns a JSHandle for the predicate’s result. Most tests only need to await completion; read the handle only when the value itself is required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can I use waitForFunction inside an iframe?

Run the wait on the relevant Frame object rather than the main Page, so the predicate executes in the document that owns the condition.

Is locator.waitForFunction available in older Playwright versions?

It was added in Playwright v1.62. Upgrade if your installed version does not expose the method.

Should I increase the timeout when a wait fails intermittently?

First verify the predicate, frame, locator, and application state. Increase the timeout only when the operation’s legitimate worst-case duration requires it; do not use a larger value to mask an incorrect condition.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.