microsoft/playwright · error · ProtocolError

closed

closed

Error message

Target page, context or browser has been closed

What it means

WKConnection.send() refuses to dispatch a protocol command when the session is in a terminal state: _crashed, _disposed, or the browser-level disconnected logs are set. It throws a ProtocolError with code 'closed' (or 'crashed'), carrying the browser-disconnect log payload so callers see why the target died.

Source

Thrown at packages/playwright-core/src/server/webkit/wkConnection.ts:125

  private _disposed = false;
  private readonly _rawSend: (message: any) => void;
  private readonly _callbacks = new Map<number, { resolve: (o: any) => void, reject: (e: ProtocolError) => void, error: ProtocolError }>();
  private _crashed: boolean = false;

  constructor(connection: WKConnection, sessionId: string, rawSend: (message: any) => void) {
    super();
    this.setMaxListeners(0);
    this.connection = connection;
    this.sessionId = sessionId;
    this._rawSend = rawSend;
  }

  async send<T extends keyof Protocol.CommandParameters>(
    method: T,
    params?: Protocol.CommandParameters[T]
  ): Promise<Protocol.CommandReturnValues[T]> {
    if (this._crashed || this._disposed || this.connection._browserDisconnectedLogs)
      throw new ProtocolError(this._crashed ? 'crashed' : 'closed', undefined, this.connection._browserDisconnectedLogs);
    const id = this.connection.nextMessageId();
    const messageObj = { id, method, params };
    this._rawSend(messageObj);
    return new Promise<Protocol.CommandReturnValues[T]>((resolve, reject) => {
      this._callbacks.set(id, { resolve, reject, error: new ProtocolError('error', method) });
    });
  }

  sendMayFail<T extends keyof Protocol.CommandParameters>(method: T, params?: Protocol.CommandParameters[T]): Promise<Protocol.CommandReturnValues[T] | void> {
    return this.send(method, params).catch(error => debugLogger.log('error', error));
  }

  markAsCrashed() {
    this._crashed = true;
  }

  isDisposed(): boolean {
    return this._disposed;

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Ensure all page/context work is awaited before calling close(); structure teardown with Promise.all over in-flight ops first.
  2. Guard sends with page.isClosed()/context.browser() checks, or wrap in try/catch and ignore 'closed' codes during teardown.
  3. Investigate the actual crash: this error is a symptom; the disconnect logs in the error payload tell you why the target died.

Example fix

// before
await page.click('#x');
await browser.close();
await page.title(); // throws closed

// after
const title = await page.title();
await browser.close();
Defensive patterns

Strategy: try-catch

Validate before calling

function canSend(page) { return !page.isClosed(); }
if (canSend(page)) await page.title();

Type guard

null

Try / catch

try { await page.title(); }
catch (e) {
  if (e.name === 'TargetClosedError' || /has been closed/.test(e.message)) return; // teardown race
  throw e;
}

Prevention

When it happens

Trigger: Any protocol send after page.close()/context.close()/browser.close() resolves, after a tab crash, or after the browser process exits. Commonly surfaces when an awaited operation races with teardown — e.g. an evaluate or screenshot completing after the test called close.

Common situations: Teardown races in afterEach hooks, operations on a page whose browser crashed, reusing a context after browser.disconnect(), or fire-and-forget code that runs past close(). On WebKit this is the unified 'target gone' signal.

Related errors


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