Recommended Free Tools
Short answer: page.title() only reads the main frame’s title; it does not wait for navigation. If your script appears to hang, identify which promise is still pending, arm navigation waits before clicks, choose a lifecycle condition your page can actually reach, and keep a finite timeout. A reliable click flow is Promise.all([page.waitForNavigation(...), page.click(...)]), followed by await page.title().
What page.title() actually does
Puppeteer’s page.title() method returns a Promise<string>. It is a shortcut for page.mainFrame().title(): it reads the title of the page’s main frame. It is not a navigation waiter and does not replace page.goto(), page.waitForNavigation(), or an application-state check.
That distinction changes the diagnosis. In code such as:
await page.goto(url);
const title = await page.title();
the pending operation may be goto(), not title(). If goto() has resolved and the title call alone remains pending, capture a minimal reproduction and inspect the page and frame state rather than assuming that the title API is waiting for a load event.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
#1 Best Overall
Find the promise that is really hanging
Add a log immediately before and after every awaited operation. Record the URL at each boundary so a redirect, single-page-app transition, or unexpected frame is visible.
const browser = await puppeteer.launch();
const page = await browser.newPage();
console.log('before goto', page.url());
await page.goto('https://example.com', {waitUntil: 'domcontentloaded'});
console.log('after goto', page.url());
console.log('before title');
const title = await page.title();
console.log('after title', JSON.stringify(title));
await browser.close();
- If “after goto” never prints, investigate navigation completion, redirects, the selected
waitUntil, and the navigation timeout. - If “after goto” prints but “after title” does not, save the exact Puppeteer version, browser executable, operating system, URL type, and a minimal script. The documented title operation itself is only a main-frame title read.
- Use
page.url()before and after the action. A changed URL does not necessarily mean that a new document response exists.
Arm a navigation wait before a click
The most common race is starting the click first and waiting afterward. A fast navigation can begin and finish before the waiter is attached. Start both promises together:
const [response] = await Promise.all([
page.waitForNavigation({waitUntil: 'domcontentloaded'}),
page.click('a.my-link'),
]);
const title = await page.title();
console.log({url: page.url(), title, response});
Promise.all evaluates its entries before awaiting either one, so the navigation listener is armed before the click can trigger navigation. If the click can fail, the combined promise rejects and you can report the original error instead of continuing with a stale page.
The documented default for waitUntil is 'load'. The example uses 'domcontentloaded' only when DOM availability is sufficient and the page’s load event is delayed by resources that are not needed for the next step.
Choose the right navigation completion condition
Navigation completion is a workflow decision, not a universal “wait longer” setting.
Rank #2
| Condition | Use when | Important behavior |
|---|---|---|
load |
You need the document’s load lifecycle to finish. | This is the documented default; analytics, ads, fonts, or other resources can delay it. |
domcontentloaded |
The HTML has been parsed and your next action needs the DOM. | It can complete before images and other subresources finish. |
| An array of events | You genuinely require several lifecycle milestones. | Every event supplied must fire; one event that never occurs keeps the wait pending until timeout. |
| Selector or app-state wait | A single-page app renders useful content after the URL transition. | Wait for the element or state your task needs instead of treating network quiet as proof of readiness. |
Network activity can continue after the useful DOM is ready. Conversely, an app may show a loading shell while the network is quiet. Select the condition based on the outcome you need: document completion, DOM availability, a selector, or a separate application-state signal.
await Promise.all([
page.waitForNavigation({waitUntil: ['domcontentloaded', 'load']}),
page.click('a.my-link'),
]);
await page.waitForSelector('[data-page-ready]');
const title = await page.title();
Only use an event array when both events are part of your requirement. Otherwise, the stricter condition can create an avoidable timeout.
Understand anchor and History API navigation
Puppeteer treats regular document navigations, anchor changes, and History API URL changes as navigation activity. For an anchor or client-side route change, waitForNavigation() may resolve with null. That is expected and does not, by itself, indicate failure: there may be no new document response to return.
const [response] = await Promise.all([
page.waitForNavigation({waitUntil: 'domcontentloaded'}),
page.click('[data-route="reports"]'),
]);
console.log('navigation response:', response); // may be null
console.log('current URL:', page.url());
await page.waitForSelector('[data-page="reports"]');
console.log('title:', await page.title());
For a single-page application, verify both the expected URL and the rendered state. Do not write code that rejects every null response unless your workflow specifically requires a new document.
Use finite navigation timeouts
Puppeteer’s documented wait options use a 30-second timeout by default. Navigation-related operations such as goto, reload, setContent, and waitForNavigation are governed by the page’s default navigation timeout. You can set a limit explicitly:
page.setDefaultNavigationTimeout(45_000);
await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 45_000,
});
Increasing the limit is appropriate when the target is predictably slow and you have evidence that it eventually reaches the selected condition. It does not repair a waiter attached after a click, an event that the page never fires, a selector that never appears, or a browser compatibility problem. Keep the timeout finite so failures produce a useful boundary and can be retried or reported.
A complete, defensive click-to-title example
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
page.setDefaultNavigationTimeout(30_000);
await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
});
console.log('start:', page.url());
const navigation = page.waitForNavigation({
waitUntil: 'domcontentloaded',
});
const click = page.click('a.my-link');
const [response] = await Promise.all([navigation, click]);
console.log('response:', response ? response.status() : null);
console.log('url:', page.url());
await page.waitForSelector('main');
console.log('title:', await page.title());
} finally {
await browser.close();
}
The explicit variables make the ordering obvious, while finally prevents a failed wait from leaving Chromium running. If the link triggers only History API routing, expect response to be null and use the URL and content checks as the success criteria.
Free tools Windows power users keep installed
One-click scans. No signup required.
Common failure modes and fixes
The waiter starts after the click
Symptom: the click appears to work, then waitForNavigation() times out. Fix: use the documented Promise.all pattern, with the waiter listed before the action.
load never arrives
Symptom: the page is visibly usable but the wait reaches its timeout. Fix: choose domcontentloaded, a selector, or an application-ready signal if that is what the task requires. Do not add multiple lifecycle events casually; all of them must fire.
A null response is treated as an error
Symptom: a client-side route or anchor change is reported as a failed navigation. Fix: accept a null response, then verify page.url() and the expected page state.
Rank #4
The timeout is increased indefinitely
Symptom: jobs take longer but still fail unpredictably. Fix: keep a finite timeout and determine whether the problem is a race, an impossible lifecycle condition, a missing selector, or genuinely slow infrastructure.
A different browser executable is used
Puppeteer guarantees operation with its bundled browser. A system-installed or otherwise different executable is a compatibility variable and is used at your own risk according to Puppeteer’s compatibility guidance. Record the executable path and browser version in the reproduction; first retest with the bundled browser.
The page is in a different frame
page.title() reads the main frame. If the visible content is inside an iframe, inspect that frame explicitly and wait for its state; changing the main-frame title call will not make an iframe’s title become the page title.
Build a useful minimal reproduction
- Puppeteer package version (the official API pages identify version 25.12.0 for several interfaces; your installed version controls actual behavior).
- Bundled versus custom browser executable and its version.
- Operating system and headless or headed mode.
- Exact URL class: full document navigation, redirect, anchor, or History API route.
- The precise order of
goto, click, navigation wait, selector wait, andtitle. - Every timeout and
waitUntilvalue, plus the complete error text. - Logs immediately before and after each awaited operation and the URL at each point.
This information distinguishes a title-read problem from a navigation race or an environment mismatch. There is no single root cause that can be confirmed from the symptom alone.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
If your actual goal is a clean image or PDF of a URL rather than browser automation, ScreenshotNeo provides a one-request screenshot API. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the result with X-Page-Verdict and X-Billed headers. It also offers an MCP server for AI agents, with take_screenshot, get_page_info, and capture_pdf tools.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →See the complete parameter reference in the ScreenshotNeo documentation. cURL:
Best Value
- Used Book in Good Condition
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}`);
Every plan includes the features: full-page lazy-image capture, CSS-selector element shots, dark mode, 12 device presets plus custom viewports, retina scale, PDF paper and page controls, HTML/CSS rendering, custom JavaScript and CSS, clicks, hidden selectors, selector/delay/network-idle waits, request and resource blocking, headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, configurable-TTL caching, signed links, asynchronous jobs with signed webhooks, bulk capture for 100 URLs per call, a usage API, OpenAPI, and compatible parameter names used by other screenshot APIs. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Sign up free.
FAQ
Does page.title() wait for the page to finish loading?
No. It reads the main frame’s current title and should be considered separate from navigation completion.
Should I always use networkidle?
No. Select the condition that matches the required outcome; ongoing requests can make network-idle conditions unsuitable for pages that are otherwise ready.
What should I do when a title is empty?
Check the correct frame and confirm that the application has rendered or updated its title before reading it; an empty string can be a valid current title state.
Frequently Asked Questions
Does page.title() wait for the page to finish loading?
No. It reads the main frame’s current title and should be considered separate from navigation completion.
Should I always use networkidle?
No. Select the condition that matches the required outcome; ongoing requests can make network-idle conditions unsuitable for pages that are otherwise ready.
What should I do when a title is empty?
Check the correct frame and confirm that the application has rendered or updated its title before reading it; an empty string can be a valid current title state.
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.




