microsoft/playwright · error · Error
Unable to retrieve content because the page is navigating an
Error message
Unable to retrieve content because the page is navigating and changing the content.
What it means
Thrown by Frame._content() when the call to read document.documentElement.outerHTML via utilityContext().evaluate() fails for any reason that is not classified as non-retriable. This typically happens when a navigation starts while the serialized HTML is being read, destroying the execution context. The error is a catch-all wrapper replacing the underlying protocol error with a user-friendly message.
Source
Thrown at packages/playwright-core/src/server/frames.ts:962
async content(progress: Progress): Promise<string> {
return progress.race(this._content());
}
private async _content(): Promise<string> {
try {
const context = await this.utilityContext();
return await context.evaluate(() => {
let retVal = '';
if (document.doctype)
retVal = new XMLSerializer().serializeToString(document.doctype);
if (document.documentElement)
retVal += document.documentElement.outerHTML;
return retVal;
});
} catch (e) {
if (this.isNonRetriableError(e))
throw e;
throw new Error(`Unable to retrieve content because the page is navigating and changing the content.`);
}
}
async setContent(progress: Progress, html: string, options: types.NavigateOptions): Promise<void> {
const tag = `--playwright--set--content--${createGuid()}--`;
await this.raceNavigationAction(progress, async () => {
const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
progress.log(`setting frame content, waiting until "${waitUntil}"`);
const context = await progress.race(this.utilityContext());
const tagPromise = new ManualPromise<void>();
this._page.frameManager._consoleMessageTags.set(tag, () => {
// Clear lifecycle right after document.open() - see 'tag' below.
this._onClearLifecycle();
tagPromise.resolve();
});
progress.setAllowConcurrentOrNestedRaces(true);
const lifecyclePromise = progress.race(tagPromise).then(() => this.waitForLoadState(progress, waitUntil));
const contentPromise = progress.race(context.evaluate(({ html, tag }) => {View on GitHub (pinned to c8fc3bf8d3)
Solutions
- Await the navigation before reading content: await page.waitForLoadState('domcontentloaded'); then await page.content().
- Use page.waitForFunction(() => document.readyState === 'complete') after the action that triggers navigation, then call content().
- Wrap content() in a retry loop with a small delay, since transient navigation races are often self-correcting.
- If you need the HTML of the pre-navigation page, call page.content() before triggering the navigation, not after.
Example fix
// before
await page.click('#navigate-away');
const html = await page.content(); // throws [320]
// after
await Promise.all([
page.waitForNavigation(),
page.click('#navigate-away'),
]);
const html = await page.content(); Defensive patterns
Strategy: retry
Validate before calling
// Ensure navigation has settled before reading content
async function safeContent(page) {
await page.waitForLoadState('domcontentloaded');
return page.content();
} Try / catch
try {
const html = await page.content();
} catch (e) {
if (e.message.includes('navigating and changing the content')) {
await page.waitForLoadState('domcontentloaded');
return page.content(); // retry once
}
throw e;
} Prevention
- Always await navigation or waitForLoadState before calling page.content().
- Use page.waitForFunction(() => document.readyState === 'complete') for SPA route changes.
- Capture content before triggering navigation if you need the pre-navigation HTML.
When it happens
Trigger: Calling page.content() or frame.content() during or immediately after triggering a navigation (page.goto, click that navigates, form submit, meta-refresh, client-side router). The evaluate() round-trip to serialize the DOM fails because the document it was reading is torn down by the ongoing navigation. Also occurs with SPA route changes that call document.open()/document.write() concurrently.
Common situations: Test clicks a link and immediately calls page.content() without awaiting navigation. SPA test calls content() right after clicking a tab that triggers a route change. Race between a download or file dialog and content retrieval. setRedirect or server-side redirect chains where the page navigates multiple times in quick succession.
Related errors
- ${name}: expected one of (load|domcontentloaded|networkidle|
- Cannot find command to respond: ${id}
- Cannot find object to "${method}": ${guid}
- Unknown new child: ${params.guid}
- Cannot find parent object ${parentGuid} to create ${guid}
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/880bdcd10dc07773.
Report an issue: GitHub.