How to Navigate to a URL Inside a Puppeteer Frame
Use Puppeteer’s `Frame.goto()` to navigate a specific iframe without changing the page’s main browsing context. Learn how to find the frame, wait for it, and handle navigation results and errors.
To navigate to a URL inside an iframe, find its Puppeteer Frame and call await frame.goto(url, options). This changes that frame’s browsing context. Use page.goto(url, options) when you want to navigate the main page.
A Puppeteer page has a frame tree: the main frame can contain child frames, and frames can be nested. You can search page.frames() for the right frame or traverse from page.mainFrame(). The destination should be a valid absolute URL with a scheme such as https://. See the official Frame API, Page API, and Frame.goto() reference.
Navigate a frame by its current URL
This runnable example opens a page, finds an attached child frame by its current URL, and navigates that frame. Replace the example URLs with the page and destination you need. Install Puppeteer in your project with npm install puppeteer.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const frame = page.frames().find(candidate =>
candidate !== page.mainFrame() &&
candidate.url() === 'https://example.com/embedded'
);
if (!frame) {
throw new Error('Target frame was not found');
}
const response = await frame.goto('https://example.org/inside-frame', {
waitUntil: 'domcontentloaded',
timeout: 30000
});
if (response === null) {
console.log('Navigation succeeded without an HTTP response object');
} else {
console.log('Final URL:', response.url());
console.log('HTTP status:', response.status());
}
} finally {
await browser.close();
}
})().catch(error => {
console.error(error);
process.exitCode = 1;
});
frame.goto() returns a promise for HTTPResponse | null. Redirects resolve with the response for the final redirect. A navigation to about:blank or to the same URL with only a changed hash can resolve to null, so check the result before reading its status.
Choose and wait for the right frame
Frame URLs and names depend on the page. A frame may be created asynchronously, may redirect, or may expose a URL different from the one you expected. Inspect page.frames(), and select by a reliable property for your page, such as a current URL or frame name.
// Inspect the attached frame tree.
for (const frame of page.frames()) {
console.log({
url: frame.url(),
name: frame.name(),
isMainFrame: frame === page.mainFrame()
});
}
If the iframe is attached later, wait for a matching frame with page.waitForFrame() rather than querying too early. Its predicate should match the identifying property that applies to your page.
const frame = await page.waitForFrame(
candidate => candidate.url().includes('/embedded'),
{ timeout: 15000 }
);
const response = await frame.goto('https://example.org/inside-frame');
console.log(response?.status());
For nested frames, start at the main frame and inspect its children with childFrames(). Repeat the traversal when the target is deeper in the tree. Ensure you navigate the intended child rather than the main frame.
function findFrameByUrl(frame, expectedUrl) {
if (frame.url() === expectedUrl) return frame;
for (const child of frame.childFrames()) {
const match = findFrameByUrl(child, expectedUrl);
if (match) return match;
}
return null;
}
const target = findFrameByUrl(page.mainFrame(), 'https://example.com/nested');
if (!target) throw new Error('Nested frame was not found');
await target.goto('https://example.org/inside-nested-frame');
Navigation options and synchronization
frame.goto(url, options?) accepts navigation options. Set a timeout and choose a wait condition that fits the page. For example, domcontentloaded waits for the document’s DOM content to load; waiting for the full load event can take longer on pages with slow resources. Pick the condition based on what the next step needs, and use the installed Puppeteer version’s API reference for the supported options.
When navigation is explicitly started with frame.goto(), awaiting that call is the primary synchronization point. If an action inside a frame causes navigation indirectly, wait for that frame’s navigation around the action:
const navigation = frame.waitForNavigation({
waitUntil: 'domcontentloaded',
timeout: 30000
});
await frame.click('a.continue');
const response = await navigation;
console.log('Frame URL after action:', frame.url());
console.log('Status:', response?.status());
Use a separate navigation waiter only when an action or page behavior starts the navigation; don’t add one automatically around a direct frame.goto().
Frame navigation versus page navigation
| Call | Browsing context | Use it when |
|---|---|---|
page.goto(url) |
The page’s main frame | The destination should replace the top-level page. |
frame.goto(url) |
The selected frame | The destination belongs inside a particular iframe or nested frame. |
Calling page.goto() when you mean to update an iframe navigates the main browsing context. First locate the frame, then call its goto().
Errors, status codes, and troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| No frame matches | The iframe has not attached yet, or the URL/name predicate does not match. | Log page.frames(), verify the identifying property, and use page.waitForFrame() if attachment is asynchronous. |
| The whole page changes | The code called page.goto(), which navigates the main frame. |
Call goto() on the selected child Frame. |
| Invalid URL or navigation failure | The destination is malformed, lacks a scheme, or cannot be reached. SSL errors, an unresponsive server, a failed main resource, and blocklist or allowlist rules can also prevent navigation. | Use a complete URL such as https://example.org/path; check the certificate, server reachability, and browser/network restrictions. |
| Navigation times out | The chosen wait condition did not complete before its timeout, possibly because resources are slow or the page keeps activity open. | Choose a wait condition appropriate to the next step, set a suitable timeout, and check whether the destination is responding. |
Cannot read status of null |
The call succeeded in a documented special case without an HTTP response object. | Check for null before reading response fields. Verify the resulting frame URL when needed. |
| HTTP 404 or 500 did not throw | A valid HTTP response with an error status is different from a failed navigation; in headless-shell mode, 404 and 500 do not themselves make goto() throw. |
Inspect response.status() and handle application-level HTTP errors explicitly. |
| Action navigation is missed | The frame navigated due to an action, but code did not wait for that navigation. | Start frame.waitForNavigation() before the action, then await it and inspect the resulting URL or response. |
When debugging, check in this order: confirm the frame identity; confirm it is attached; validate the absolute destination URL; await the relevant navigation; account for a nullable response; then distinguish transport failures from HTTP error statuses.
Performance, reliability, and cost
Frame navigation has the same practical tradeoff as page navigation: wait only for the event your next operation requires. Waiting for more page activity can extend capture time or expose you to slow third-party resources. A frame can also detach or be replaced while a page is changing, so find or wait for the target near the point where you use it and handle a missing frame as an expected condition when the page is dynamic.
The Puppeteer API itself does not establish a universal runtime or cost for a particular page. Those depend on the destination, resources, browser environment, and your infrastructure. For operational reliability, use an explicit timeout, log the frame URL and response status, and distinguish navigation exceptions from non-success HTTP statuses.
Or skip the browser setup
If your goal is to get a screenshot of a URL rather than interact with a particular iframe, ScreenshotNeo provides a website screenshot API and MCP server. Its API takes one GET request with a URL and can return PNG, JPEG, WebP, or PDF. The options most screenshot APIs use also work, which can make switching easier. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each of those steps can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 screenshots.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
FAQ
Can I navigate a frame that is nested inside another iframe?
Yes. Traverse from page.mainFrame() through each frame’s childFrames(), identify the nested target, then call goto() on it.
Does frame.goto() throw for a 404?
Do not rely on that. In headless-shell mode, a valid response with status 404 or 500 does not itself cause goto() to throw. Inspect the response status.
What if the frame is created after the page loads?
Wait for a matching frame with page.waitForFrame(), using a predicate based on the frame’s URL, name, or another property that identifies it on that page.
Can I use page.goto() and still target the iframe?
page.goto() navigates the main frame. To navigate an iframe’s context, obtain its Frame and use frame.goto().


