Heuristic:Puppeteer Puppeteer Navigation Race Condition Avoidance
| Knowledge Sources | |
|---|---|
| Domains | Browser_Automation, Debugging |
| Last Updated | 2026-02-11 23:30 GMT |
Overview
Always start `waitForNavigation()` before the action that triggers navigation (e.g., `click()`) using `Promise.all()` to avoid race conditions.
Description
A common Puppeteer pitfall is calling `click()` on a link or button that triggers a page navigation, then calling `waitForNavigation()` afterward. If the navigation completes before `waitForNavigation()` is called, the promise will never resolve (or will time out) because the navigation event was already fired and missed. The correct pattern is to start waiting for navigation before triggering the action.
Usage
Apply this heuristic any time a user action triggers a page navigation: clicking links, submitting forms, calling `evaluate()` with `location.href` changes, or any other action that causes the browser to navigate. This is one of the most common sources of timeout errors in Puppeteer scripts.
The Insight (Rule of Thumb)
- Action: Use `Promise.all([page.waitForNavigation(), page.click(selector)])` to start the navigation listener before the click fires.
- Value: Eliminates the most common source of non-deterministic timeout failures in Puppeteer scripts.
- Trade-off: Slightly more verbose code. Alternatively, use the Locator API which handles this automatically.
Reasoning
The browser processes navigation events asynchronously. When `click()` triggers a navigation:
- The click fires a navigation event in the browser
- The navigation event is emitted to listeners
- If no listener is registered yet, the event is lost
By calling `waitForNavigation()` first (inside `Promise.all`), the listener is registered before the click fires, guaranteeing the navigation event is captured.
Incorrect pattern (from Puppeteer API docs):
// WRONG — Race condition: navigation may complete before waitForNavigation
const navPromise = page.waitForNavigation();
await page.click('a.my-link');
await navPromise; // May timeout if navigation already finished
Correct pattern (from `packages/puppeteer-core/src/api/Page.ts:2803-2815`):
// CORRECT — Start waiting before triggering navigation
const [response] = await Promise.all([
page.waitForNavigation(waitOptions),
page.click(selector, clickOptions),
]);
This pattern is documented in the JSDoc for `Frame.waitForNavigation()` at `packages/puppeteer-core/src/api/Frame.ts:1040-1053`.