microsoft/playwright · error · Error

Root session cannot be closed

Error message

Root session cannot be closed

What it means

Thrown by CRConnection.detach() (crConnection.ts:180) when this._parentSession is falsy. The root browser-level session has no parent to detach from, so Target.detachFromTarget cannot apply. Root sessions are obtained from _clientRootSession() and are not meant to be detached by users.

Source

Thrown at packages/playwright-core/src/server/chromium/crConnection.ts:180

        callback.resolve(object.result);
      }
    } else if (object.id && object.error?.code === -32001) {
      // Message to a closed session, just ignore it.
    } else {
      assert(!object.id, object?.error?.message || undefined);
      Promise.resolve().then(() => {
        if (this._eventListener)
          this._eventListener(object.method!, object.params);
        (this.emit as any)(object.method as any, object.params);
      });
    }
  }

  async detach() {
    if (this._closed)
      throw new Error(`Session already detached. Most likely the page has been closed.`);
    if (!this._parentSession)
      throw new Error('Root session cannot be closed');
    // Ideally, detaching should resume any target, but there is a bug in the backend,
    // so we must Runtime.runIfWaitingForDebugger first.
    await this._sendMayFail('Runtime.runIfWaitingForDebugger');
    await this._parentSession.send('Target.detachFromTarget', { sessionId: this._sessionId });
    this.dispose();
  }

  dispose() {
    this._closed = true;
    this._connection._sessions.delete(this._sessionId);
    this._rejectPendingCallbacks(`Internal server error, session closed.`);
  }

  private _rejectPendingCallbacks(message: string) {
    for (const callback of this._callbacks.values()) {
      callback.error.setMessage(message);
      callback.error.type = this._crashed ? 'crashed' : 'closed';
      callback.error.logs = this._connection._browserDisconnectedLogs;

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Do not detach the root session; only detach sessions created via newCDPSession(page|frame).
  2. Before detaching generically, check that the session has a parent (skip the root).
Defensive patterns

Strategy: validation

Validate before calling

// Only detach sessions you created via newCDPSession.
// If walking sessions generically, skip the root:
// root sessions have no parent — do not call detach() on them.
const isChildSession = (s: CDPSession) => /* track via your own registry */ true;
if (isChildSession(session)) await session.detach();

Prevention

When it happens

Trigger: Calling detach() on the browser's root CDP client; generic session-walking tooling that detaches every session it finds, including the root; reaching the root after a parent chain was nulled.

Common situations: Internal tooling that lists _connection._sessions and detaches each; user code that grabbed the browser-level client for an unrelated command and then tries to detach.

Related errors


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