If a Puppeteer page never finishes loading, first identify which promise is still pending. A stalled page.goto(), waitForNavigation(), waitForNetworkIdle(), locator wait, or request-interception handler represents a different condition and needs a different fix. Start by checking request interception, then verify navigation sequencing, choose a wait that matches your actual milestone, and keep every wait bounded by a finite timeout.
Find the operation that is actually waiting
There is no single Puppeteer signal called “page complete.” Navigation, network-idle, element visibility and application state are separate milestones. Log immediately before and after each potentially pending call so you can identify the unresolved operation.
console.log('before goto');
await page.goto('https://example.com');
console.log('after goto');
Repeat this around waitForNavigation(), waitForNetworkIdle(), locator waits and any asynchronous code inside a request event handler. Note whether the call eventually throws a timeout or remains pending indefinitely. A timeout identifies the condition that was not reached; it does not, by itself, identify why.
Fix request interception first
If your code calls page.setRequestInterception(true), every intercepted request pauses until Puppeteer resolves it. The official guidance is direct: “Puppeteer requires request.continue() to be called explicitly or the request will hang.” Every branch must call exactly one of continue(), respond() or abort(), unless the browser completes the request from cache.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →#1 Best Overall
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
Allow requests by default
await page.setRequestInterception(true);
page.on('request', request => {
if (request.resourceType() === 'image') {
return request.abort();
}
// The default branch must resolve the request.
return request.continue();
});
A common hang is an early return that does nothing:
// Bug: this leaves matching requests unresolved.
page.on('request', request => {
if (request.url().includes('/analytics')) return;
request.continue();
});
Resolve the branch explicitly instead:
page.on('request', request => {
if (request.url().includes('/analytics')) {
return request.abort();
}
return request.continue();
});
Guard against multiple handlers
Another listener, test helper or third-party package may resolve the same interception first. Check request.isInterceptResolutionHandled() immediately before resolving. In an asynchronous handler, check again after every await, because another listener can act while your handler is suspended.
page.on('request', async request => {
if (request.isInterceptResolutionHandled()) return;
const shouldBlock = request.url().includes('tracker');
await Promise.resolve(); // asynchronous policy lookup, for example
if (request.isInterceptResolutionHandled()) return;
if (shouldBlock) {
await request.abort();
} else {
await request.continue();
}
});
Keep the final check and the resolution call together. If you have several listeners, temporarily disable interception or remove listeners to confirm whether the handler is the source of the stall.
Rank #2
- Brand: Wiley
- Set of 2 Volumes
- A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
Pair a navigation wait with the action that triggers it
When a click submits a form or follows a link, arm the navigation wait and perform the action in the same Promise.all. Starting the click first can allow the navigation event to occur before waitForNavigation() is listening.
const [response] = await Promise.all([
page.waitForNavigation(),
page.click('a.my-link'),
]);
console.log('navigation response:', response ? response.status() : 'null');
waitForNavigation() watches for a new URL or a reload and returns the main-resource response. Anchor changes and History API navigation can resolve with null; that is a completed navigation wait, not an indication that the promise is still stuck.
Use the same pattern for form submission
const [response] = await Promise.all([
page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
page.click('button[type="submit"]'),
]);
Choose navigation options that match the application. If the button updates the current document through fetch or the History API rather than loading a new document, a navigation wait may be the wrong condition. Wait for the resulting UI state instead.
Rank #3
Choose a wait condition that matches the work
| Wait strategy | What it represents | Use it when | Main caveat |
|---|---|---|---|
waitForNavigation() |
A new URL or reload; returns the main-resource response, or null for anchor/History API cases |
An action is expected to navigate | Pair it with the action using Promise.all to avoid a race |
waitForNetworkIdle() |
Network activity satisfies the configured idle condition | The next operation genuinely requires network quiet | Long-polling, analytics or streaming can prevent idle; readiness may occur earlier |
| Locator or function wait | A specific element or application condition is satisfied | The next step needs a known UI state | The selector or predicate must describe the real milestone |
| Navigation timeout | A maximum duration for navigation-related calls | Every navigation should fail within a known bound | It bounds waiting but does not repair a slow or unresolved operation |
Network idle is not universal readiness
waitForNetworkIdle() is useful for pages that become stable after requests settle, but it is a poor fit for applications that poll continuously or keep a socket open. The current options reference lists concurrency defaulting to 0 and idleTime defaulting to 500 milliseconds; verify these defaults against the Puppeteer version installed in your project.
await page.waitForNetworkIdle({
concurrency: 0,
idleTime: 500,
timeout: 15000,
});
If your task is “click the Export button once it is visible,” wait for that button rather than for every request to stop.
Recommended Free Tools
const exportButton = page.getByRole('button', { name: 'Export' });
await exportButton.wait({ state: 'visible', timeout: 10000 });
await exportButton.click();
A lower-level alternative is:
await page.waitForSelector('[data-testid="export-ready"]', {
visible: true,
timeout: 10000,
});
For state that has no single element, use a function-based condition:
Rank #4
await page.waitForFunction(
() => document.body.dataset.status === 'ready',
{ timeout: 10000 }
);
Set finite timeouts and capture evidence
Set a navigation timeout so a missing milestone becomes an actionable error instead of an indefinitely pending test.
page.setDefaultNavigationTimeout(30000);
try {
await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
});
} catch (error) {
console.error('Navigation failed:', error);
}
This setting affects navigation-related methods including goto, reload, setContent and waitForNavigation. Raising the number only changes the bound; disabling timeouts can hide the original diagnosis. Use a longer value only when you have evidence that the target legitimately needs more time.
Log requests while reproducing
page.on('request', request => {
console.log('request', request.method(), request.url());
});
page.on('requestfailed', request => {
console.warn('failed', request.url(), request.failure());
});
page.on('response', response => {
if (response.status() >= 400) {
console.warn('HTTP', response.status(), response.url());
}
});
Compare the last logged request with the pending promise. A request that never receives a resolution points to interception; a completed document with a missing selector points to the selector or application state; repeated requests point to a network-idle mismatch.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Best Value
- JavaScript Jquery
- Introduces core programming concepts in JavaScript and jQuery
- Uses clear descriptions, inspiring examples, and easy-to-follow diagrams
Common symptoms and targeted fixes
page.goto() times out
- Check whether interception is enabled and every handler branch resolves.
- Try
waitUntil: 'domcontentloaded'when waiting for all subresources is unnecessary. - Inspect failed requests, redirects and authentication requirements.
- Keep the timeout finite and investigate the specific request or lifecycle event that did not complete.
waitForNavigation() never resolves after a click
- Use
Promise.allwith the wait created before or alongside the click. - Confirm that the click really causes a document navigation; single-page apps may only change history or render new content.
- If it is an anchor or History API transition, expect a
nullresponse and wait for the destination UI.
waitForNetworkIdle() times out
- Look for polling, analytics, advertisements, WebSockets or streaming requests.
- Replace network idle with a locator or function wait for the state your script needs.
- If network quiet is essential, configure an appropriate concurrency and idle period and retain a finite timeout.
The page appears loaded but the next action fails
- Wait for the exact element to be visible or enabled.
- Wait for an application state attribute or a function predicate.
- Check that the selector describes the post-load state rather than a placeholder rendered before data arrives.
Or skip the browser setup
If your goal is a clean image or PDF rather than browser automation, ScreenshotNeo provides a GET endpoint that captures a URL as PNG, JPEG, WebP or PDF. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing status. It also offers an MCP server with take_screenshot, get_page_info and capture_pdf for Claude, Cursor and other MCP clients.
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 full option list and request details in the ScreenshotNeo documentation. Every plan includes its features: full-page and element capture, device presets, custom CSS and JavaScript, waits, request blocking, cookies and headers, PDFs, caching, signed links, asynchronous jobs, bulk capture and a usage API. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Version and diagnosis caveats
Puppeteer APIs and defaults can change. The reference surfaced for this guidance was Puppeteer 25.12.0, while the navigation-timeout reference used a “next” documentation path. Check the API reference for the exact version in your package before copying version-sensitive options. Without the target URL, source code, logs and request trace, no single cause can be confirmed for an individual run.
Frequently Asked Questions
Does a successful HTTP response prove that Puppeteer is finished?
No. The response covers the navigation resource, while scripts may still render content, poll APIs or wait for client-side state. Wait for the milestone your next operation requires.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Why can a navigation response be null?
Anchor changes and History API navigation can complete without a new main-resource response, so Puppeteer documents a null result for those cases.
Should I disable Puppeteer timeouts while debugging?
No. A finite timeout exposes the pending condition. Disabling it can leave a broken wait unresolved and remove the error that would help locate the cause.
Quick Recap
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.




