microsoft/playwright · error · TargetClosedError
Target page, context or browser has been closed
Error message
Target page, context or browser has been closed
What it means
navigateFrame() checks pageProxySession.isDisposed() before sending Playwright.navigate; if the page session is already torn down it throws TargetClosedError carrying the page's recorded closeReason. This is the WebKit equivalent of 'navigation attempted on a closed page'.
Source
Thrown at packages/playwright-core/src/server/webkit/wkPage.ts:530
worldName = 'utility';
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
if (worldName)
frame.contextCreated(worldName, context);
this._contextIdToContext.set(contextPayload.id, context);
}
private async _onBindingCalled(contextId: Protocol.Runtime.ExecutionContextId, argument: string) {
const pageOrError = await this._page.waitForInitializedOrError();
if (!(pageOrError instanceof Error)) {
const context = this._contextIdToContext.get(contextId);
if (context)
await this._page.onBindingCalled(argument, context);
}
}
async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult> {
if (this._pageProxySession.isDisposed())
throw new TargetClosedError(this._page.closeReason());
const pageProxyId = this._pageProxySession.sessionId;
const result = await this._pageProxySession.connection.browserSession.send('Playwright.navigate', { url, pageProxyId, frameId: frame._id, referrer });
return { newDocumentId: result.loaderId };
}
_onConsoleMessage(event: Protocol.Console.messageAddedPayload) {
// Note: do no introduce await in this function, otherwise we lose the ordering.
// For example, frame.setContent relies on this.
const { type, level, text, parameters, url, line: lineNumber, column: columnNumber, source } = event.message;
if (level === 'error' && source === 'javascript') {
const { name, message } = splitErrorMessage(text);
let stack: string;
if (event.message.stackTrace) {
stack = text + '\n' + event.message.stackTrace.callFrames.map(callFrame => {
return ` at ${callFrame.functionName || 'unknown'} (${callFrame.url}:${callFrame.lineNumber}:${callFrame.columnNumber})`;
}).join('\n');
} else {View on GitHub (pinned to c8fc3bf8d3)
Solutions
- Await all navigations before closing; do not navigate in teardown after close.
- Guard with page.isClosed(); skip navigation when true.
- Wrap navigation in try/catch and treat TargetClosedError as a clean stop during teardown.
Example fix
// before await page.goto(url); // page already closed // after if (!page.isClosed()) await page.goto(url);
Defensive patterns
Strategy: try-catch
Validate before calling
if (!page.isClosed()) await page.goto(url);
Type guard
null
Try / catch
try { await page.goto(url); }
catch (e) {
if (e.name === 'TargetClosedError' || /has been closed/.test(e.message)) return; // expected during teardown
throw e;
} Prevention
- Do not navigate after close() in hooks.
- Guard navigations with page.isClosed().
When it happens
Trigger: Calling page.goto()/frame.goto() (or any navigation-triggering API) after page.close() began, after a crash, or during teardown. Also from locator.click() that resolves a navigation on a closing page.
Common situations: Navigating in a hook after the page was closed in the test body. Fire-and-forget navigations racing with context teardown. Re-navigating a page whose tab crashed.
Related errors
- closed
- ${this._closeReason}
- Target page, context or browser has been closed
- ${name}: expected one of (load|domcontentloaded|networkidle|
- The object has been collected to prevent unbounded heap grow
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/a567faeb797bddc8.
Report an issue: GitHub.