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

  1. Await all navigations before closing; do not navigate in teardown after close.
  2. Guard with page.isClosed(); skip navigation when true.
  3. 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

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


AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12). Data as JSON: /api/errors/a567faeb797bddc8. Report an issue: GitHub.