Driver FixRecommendedSound, Wi-Fi or graphics acting up? Check drivers firstFind missing or outdated drivers fast.Check DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix Now×
Skip to content
browser automation

Why Puppeteer page.click() Does Not Wait—and How to Fix It

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

Short answer: Puppeteer’s page.click() waits only until the click operation succeeds. It does not wait for a new document, a History API change, an asynchronous data request, or a single-page-app render that the click might trigger. Start the wait that matches the expected result at the same time as the click.

For a normal navigation or reload, use Promise.all([page.waitForNavigation(), page.click(...)]). For an in-page update, wait for a result-specific selector, text change, or predicate instead. A wait created after the click can miss a fast navigation.

What page.click() actually guarantees

Puppeteer’s Page.click() finds the selector, scrolls the element into view when needed, and uses the page mouse to click its center. Its promise resolves when that interaction has completed. It rejects when no matching element exists or the action cannot be performed.

The promise is not a general “wait until the page is done” signal. A click may start navigation, submit a form, update the URL with the History API, fetch data, open a menu, or trigger several asynchronous framework renders. Those consequences have different completion signals, so Puppeteer leaves the choice to your code.

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

“Bear in mind that if click() triggers a navigation event and there’s a separate page.waitForNavigation() promise to be resolved, you may end up with a race condition that yields unexpected results.”

— Puppeteer documentation

The API behavior described here matches the Puppeteer 25.12.0 documentation identified on September 29, 2026. Check the documentation for the version installed in your project if a signature or default differs.

Fix a click that causes a full navigation or reload

Install the navigation waiter before (or concurrently with) the click:

const [response] = await Promise.all([
  page.waitForNavigation(),
  page.click('a.my-link'),
]);

console.log('Loaded URL:', await page.url());
console.log('Main response:', response ? response.status() : 'no response object');

Both promises are created as Promise.all evaluates its array. The navigation listener is therefore ready while the click is initiated. By contrast, this sequence is racy:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.click('a.my-link');
await page.waitForNavigation(); // Too late if navigation already began

A quick redirect or cached navigation can begin and finish before the second line installs its listener, leaving the script waiting until timeout.

Set an appropriate navigation timeout

page.setDefaultNavigationTimeout(30_000);

const [response] = await Promise.all([
  page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 30_000 }),
  page.click('a.my-link'),
]);

waitUntil controls which lifecycle milestone counts as navigation completion. Use the default when the document response is enough; choose a later milestone only when your page genuinely needs it. A “network idle” condition can remain unsettled on pages with analytics, polling, or long-lived connections, so do not select it automatically.

Read redirects and the returned response correctly

waitForNavigation() resolves with the main resource response. With multiple redirects, Puppeteer returns the response for the last redirect. The response can be null for a History API URL change or an anchor navigation, even though the URL changed successfully. Treat the URL or DOM state as the proof of completion in those cases, not the presence of a response object.

Choose the wait from the effect you expect

Expected effect Use Completion signal to verify
New document, form submit, or reload Promise.all([page.waitForNavigation(), page.click(...)]) URL, response status, or destination content
History API route change or anchor The same concurrent navigation pattern Changed URL or route-specific DOM; response may be null
In-place AJAX/fetch update waitForSelector(), waitForFunction(), or a text/state assertion A new or changed result that was not already present
Only making an element actionable Puppeteer locator or an element-availability wait Element is present, visible, enabled, and actionable

History API and anchor changes

Puppeteer treats History API URL changes as navigation. The document may not reload, and an anchor transition can also produce no main-resource response. Use the concurrent pattern, then assert the route or a route-specific element:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const [response] = await Promise.all([
  page.waitForNavigation(),
  page.click('[data-route="reports"]'),
]);

await page.waitForSelector('h1[data-page="reports"]');
console.log('Reports route:', await page.url());

If the application changes the URL without emitting the navigation event your version observes, wait for the observable result instead:

await page.click('[data-route="reports"]');
await page.waitForFunction(
  expected => location.pathname === expected,
  {},
  '/reports'
);

Single-page-app updates without navigation

For a filter, “Load more” button, modal, or client-side request, waitForNavigation() is the wrong condition. Wait for the state the user needs:

await page.click('button#load-more');
await page.waitForSelector('.result-card:nth-child(21)');

For changed text, capture the old value and wait until it differs:

const before = await page.$eval('.total', el => el.textContent.trim());
await page.click('button.apply-filter');
await page.waitForFunction(
  oldValue => document.querySelector('.total')?.textContent.trim() !== oldValue,
  {},
  before
);

A selector wait is only meaningful when it distinguishes the post-click state. waitForSelector() resolves immediately if the selector already exists, so waiting for a permanent container such as .results does not prove that new results arrived. Prefer a newly inserted child, a changed attribute, hidden-to-visible transition, or a predicate that checks the expected value.

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.

Waiting for action readiness

Puppeteer locators can wait until an element is present and in a state suitable for an action. That solves a different problem: whether the click can be performed. It does not promise that the application’s response to the click has finished. Use a locator (or an element wait) before the action and a result wait after it when both conditions matter.

const button = page.locator('button.save');
await button.click();
await page.waitForSelector('.toast-success');

The lower-level waitForSelector() waits for element availability and times out if the condition is not met; it does not automatically retry a failed click or infer what the click should change.

Reliable patterns for common workflows

Link to a destination page

await page.goto('https://example.com/account');
const [response] = await Promise.all([
  page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
  page.click('a[href="/settings"]'),
]);
if (response && !response.ok()) {
  throw new Error(`Navigation failed with ${response.status()}`);
}
await page.waitForSelector('main.settings');

Form submission with a redirect

await page.type('#email', '[email protected]');
const [response] = await Promise.all([
  page.waitForNavigation({ waitUntil: 'load' }),
  page.click('button[type="submit"]'),
]);
if (!response || !response.ok()) {
  throw new Error('The submit navigation did not return a successful response');
}

Modal or menu that stays on the same page

await page.click('[aria-label="Open menu"]');
await page.waitForSelector('[role="menu"]:not([hidden])');

await page.click('[role="menuitem"][data-id="billing"]');
await page.waitForSelector('#billing-panel');

Wait for a specific application condition

await page.click('button.refresh');
await page.waitForFunction(() => {
  const status = document.querySelector('[data-sync-status]');
  return status?.getAttribute('data-state') === 'complete';
});

Clicking an element that may be covered or off-screen

When the click itself fails, fix actionability first. Confirm the selector is unique, wait for visibility, scroll or dismiss an overlay, and inspect whether a disabled state is preventing interaction. Do not add a post-click navigation wait to solve an element that was never clicked.

Why fixed sleeps are a weak fix

await new Promise(resolve => setTimeout(resolve, 2000)) waits a fixed amount of time, not for the event your test needs. It can be too short on a slow run and unnecessarily long on a fast one. A selector, predicate, URL, response, or application state encodes the actual outcome and produces a useful timeout when that outcome never occurs.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Best Value
The SQL Programming Language: .
  • Used Book in Good Condition

A short delay can be appropriate for a deliberately timed animation when no observable state exists, but it should be an exception rather than the synchronization strategy for navigation or data loading.

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

Troubleshooting click-and-wait failures

“Navigation timeout exceeded”

  • Cause: The click does not navigate, the waiter was installed after the click, or the selected lifecycle event never occurs because of persistent connections.
  • Fix: Confirm the URL/document actually changes. Use concurrent Promise.all for navigation, or replace it with a selector/predicate wait for an in-page update. Choose a less strict waitUntil only when it matches your readiness requirement.

The script hangs after an apparently successful click

  • Cause: The click promise resolved, but the subsequent condition is not produced, or the wait targets an element that is already present.
  • Fix: Log the URL, inspect the DOM before and after the action, and wait for a state transition or unique post-click element.

waitForNavigation() returns null

  • Cause: A History API route change or anchor navigation may not have a main resource response.
  • Fix: Assert the changed URL or route-specific DOM instead of requiring a response object.

“No element found for selector”

  • Cause: The selector is wrong, the frame is different, or the element has not been rendered.
  • Fix: Verify the selector in the target frame, wait for the element before clicking, and inspect the page at the failure point.

The click times out or is intercepted

  • Cause: An overlay, animation, disabled control, or changing layout prevents the mouse action.
  • Fix: Wait for the control to be visible and enabled, close the overlay, and avoid forcing a click unless bypassing real user behavior is intentional. A forced action can hide a genuine page defect.

Playwright code was copied into Puppeteer

Playwright’s locator documentation describes automatic waiting behavior that often removes the need for explicit waits in its own API. That does not change Puppeteer’s Page.click() contract. In Puppeteer, explicitly synchronize the consequence of the click.

Performance and reliability considerations

  • Install event-based waits before the action so fast navigations cannot be missed.
  • Wait for the earliest lifecycle or state that proves the next operation is safe; waiting for every network request to end can make tests slower and less deterministic.
  • Use stable selectors such as data attributes or route-specific landmarks instead of styling classes that frequently change.
  • Keep timeout values explicit and aligned with the environment. A longer timeout can accommodate a slow CI runner, but it cannot fix a condition that never occurs.
  • After waiting, assert the result you actually need: URL, response status, visible text, element count, or application state. A resolved wait alone is not a business-level assertion.
  • Capture diagnostics on failure, including the current URL, a screenshot, relevant HTML, and console or network errors. This distinguishes a missed wait from a broken page.

Or skip the browser setup

If your goal is a reliable page image rather than an interactive Puppeteer workflow, ScreenshotNeo provides a single screenshot API call. It can accept consent banners before capture and remove more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Clean shots are the only billable responses: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status.

JavaScript:

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 API documentation for response options. The same endpoint supports PNG, JPEG, WebP, or PDF output, full-page captures with lazy images loaded, CSS-selector element captures, device and viewport settings, dark mode, custom CSS and JavaScript, click-before-capture actions, selector or network-idle waits, ad/tracker/request blocking, custom headers and cookies, geolocation and timezone, transparent backgrounds, resizing, selectable cache TTLs, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification. Parameter names used by other screenshot APIs also work to ease migration.

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

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)

Plans include 1,000 shots per month free with no card; paid plans start at $5 for 3,000 shots. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. Create a free ScreenshotNeo account to try the 1,000 monthly shots without a card.

Decision checklist

  1. Does the click create a new document or reload? Start waitForNavigation() and click() together.
  2. Does it change only the URL through History API or an anchor? Use the same pattern, then verify URL or route DOM; allow for a null response.
  3. Does it update the existing document? Wait for a new element, changed value, visibility transition, or predicate.
  4. Are you only trying to make the control clickable? Use a locator or element-readiness wait; do not confuse it with result synchronization.
  5. Can the condition already be true? Capture the old state or choose a post-click marker that cannot exist before the action.

Frequently Asked Questions

Can I use page.waitForTimeout() instead?

A fixed delay does not verify navigation or application state. Prefer the event or condition that proves the result; use a delay only for an unavoidable, purely timed effect.

Does page.click() wait for network requests?

No. It waits for the click operation itself, not arbitrary requests started by the page.

What if a click opens a new tab?

Wait for the browser target or page creation event and then synchronize that new page separately; waitForNavigation() on the original page is not a substitute.

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

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

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

Read next

Recommended PC Tool
Recommended PC Tool
Outdated Drivers Are Slowing You DownFree scan - exact matches
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.