How to Get a Frame’s URL in Puppeteer
Get a Puppeteer frame’s URL with `frame.url()`. Learn how to find the right frame, traverse nested frames, and handle common issues.
Call frame.url() on the Puppeteer Frame whose URL you need. It returns a string synchronously, so do not use await. Use page.url() for the main frame; use page.frames() to find attached frames, including iframes.
const frameUrl = frame.url();
console.log(frameUrl);
1. Get the main frame’s URL
page.url() is a shortcut for page.mainFrame().url(). It returns the page’s main-frame URL, not a child iframe’s URL.
const mainFrameUrl = page.url();
console.log(mainFrameUrl);
// These refer to the same frame:
const alsoMainFrameUrl = page.mainFrame().url();
2. Get an iframe URL
Use page.frames() to get the frames attached to a page, then call url() on the frame you want. This runnable example prints each frame’s URL:
const frames = page.frames();
for (const frame of frames) {
console.log(frame.url());
}
If the page contains several frames, identify the correct one using information available in your page, such as its URL or the frame’s associated element. Avoid assuming that a particular index in page.frames() always identifies the same iframe: pages can add, remove, or reorder frames.
For example, to select a frame by a known URL fragment:
const targetFrame = page.frames().find(frame =>
frame.url().includes('checkout')
);
if (!targetFrame) {
throw new Error('Checkout frame was not found');
}
console.log(targetFrame.url());
Match as specifically as your use case requires. A substring can match more than one frame, and URLs can change during navigation. If multiple frames could match, collect the matches and apply another identifying condition rather than silently choosing the first.
3. Traverse nested frames
Frames can contain child frames. To print the full hierarchy, start at the main frame and recursively visit each frame’s childFrames():
function logFrameUrls(frame, indent = '') {
console.log(indent + frame.url());
for (const child of frame.childFrames()) {
logFrameUrls(child, indent + ' ');
}
}
logFrameUrls(page.mainFrame());
Use this tree traversal when the parent-child relationships matter. If you only need a flat list of currently attached frames, page.frames() is simpler.
4. Complete runnable example
This Node.js example opens a page, prints the main-frame URL and all attached frame URLs, then closes the browser. Install Puppeteer in your project with npm install puppeteer, save the code as frames.js, and run node frames.js.
const puppeteer = require('puppeteer');
async function main() {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
});
console.log('Main frame:', page.url());
for (const [index, frame] of page.frames().entries()) {
console.log(`Frame ${index}:`, frame.url());
}
} finally {
await browser.close();
}
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
The example uses domcontentloaded so it does not wait for every image or other resource. Choose the navigation condition that fits your task; the URL read itself is synchronous, but the page may still be navigating if your code has not waited for the relevant navigation.
5. Read the URL after navigation
frame.url() reports the frame’s current URL. It does not wait for navigation or provide a historical URL. If your code needs the URL after a particular navigation, coordinate the URL read with that navigation in your flow.
const navigation = page.waitForNavigation();
await page.click('a.next-page');
await navigation;
console.log('After navigation:', page.mainFrame().url());
For an iframe navigation, wait for the event or condition that represents the iframe transition in your application, then read the relevant frame’s current URL. Do not assume that waiting for the main page’s navigation also means every child frame has completed its own navigation.
6. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
page.url() returns the top-level page address, but not the iframe address. |
page.url() refers to the main frame. |
Find the intended frame in page.frames() and call frame.url(). |
The result is a Promise-related value or the code uses await frame.url(). |
Frame.url() is synchronous and returns a string. |
Use const url = frame.url(); without await. |
| No frame matches your search. | The iframe may not be attached yet, may have navigated, or the matching condition may be too strict. | Run the lookup after the page state you need is present; inspect all current URLs with page.frames().map(frame => frame.url()) and adjust the identifying condition. |
| The first matching frame is the wrong one. | More than one frame matched a broad substring or other condition. | Collect all matches and narrow the selection using a more specific condition. Do not depend on a fixed array index. |
| A URL is read before it reaches the expected value. | The relevant frame is still navigating, or the code read the URL before the navigation it depends on. | Coordinate the read with the intended navigation or page state, then call url(). |
| A nested iframe is missing from a custom traversal. | The code inspected only direct children or only the main frame. | Recurse through each frame’s childFrames(), or inspect the flat list from page.frames(). |
Detached-frame behavior and unusual URL schemes are not specified here. If your workflow can remove a frame while it is being inspected, make frame selection and reading part of a flow that accounts for page changes rather than relying on undocumented behavior.
7. Performance, reliability, and cost
Reading a frame URL is a direct synchronous API call and does not itself navigate the page or fetch the frame’s resources. The main reliability concern is choosing the intended frame and reading it at the right point in your navigation flow. When logging many frames, expect the number of entries to reflect the page’s current attached-frame structure; nested pages can have more than one level.
For screenshot work, a browser script means you manage browser launch, navigation, and capture in your own environment. If you only need a screenshot and do not need to inspect frame URLs in application code, ScreenshotNeo provides a screenshot API and MCP server. Its API accepts one GET request for a URL and can return PNG, JPEG, WebP, or PDF. See the ScreenshotNeo website and API documentation.
8. Or skip the browser setup
To capture a page without setting up Puppeteer, make one request to ScreenshotNeo. Replace the example URL with the page you want to capture and use your API key:
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 removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed, and response headers report the page verdict and billing status. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan. Check the docs for request options.
Sign up free and get 1,000 screenshots a month with no card.
9. FAQ
Does frame.url() return a string?
Yes. The Puppeteer Frame API documents its return type as string.
Can I get every frame URL in one expression?
Yes: const urls = page.frames().map(frame => frame.url());
Does page.url() include iframe URLs?
No. It gives the main-frame URL. Read each child frame from its own Frame object.
How do I inspect URLs at every nesting level?
Start at page.mainFrame() and recursively visit each frame’s childFrames().


