microsoft/playwright · error · Error

No tab found for sessionId: ${sessionId}

Error message

No tab found for sessionId: ${sessionId}

What it means

Thrown by the extension browser model's `sendCommand` when a `sessionId` matches neither a relay-level tab session (`pw-tab-N`) nor any tab session's set of child CDP sessions (workers, oopifs). The relay cannot route a CDP command whose session it does not recognize, so it aborts.

Source

Thrown at packages/playwright-core/src/tools/mcp/browserModel.ts:192

      method,
      params,
    ]);
  }

  // Forward a CDP command from Playwright to the tab its sessionId resolves to.
  async sendCommand(sessionId: string, method: string, params: any): Promise<any> {
    // Two cases:
    // 1. sessionId is a relay-level tab session (pw-tab-N) → strip and route by tabId.
    // 2. sessionId is a child CDP session (worker, oopif) → route to its owning tab,
    //    keep the sessionId so the extension forwards it to chrome.debugger.
    let tabSession = this._findTabSession(s => s.sessionId === sessionId);
    let cdpSessionId: string | undefined;
    if (!tabSession) {
      tabSession = this._findTabSession(s => s.childSessions.has(sessionId));
      cdpSessionId = sessionId;
    }
    if (!tabSession)
      throw new Error(`No tab found for sessionId: ${sessionId}`);
    return await this._sendToExtension('chrome.debugger.sendCommand', [
      { tabId: tabSession.tabId, sessionId: cdpSessionId },
      method,
      params,
    ]);
  }

  // ─── Internals ────────────────────────────────────────────────────────

  private async _attachTab(tabId: number): Promise<TabSession> {
    const existing = this._tabSessions.get(tabId);
    if (existing)
      return existing;
    await this._sendToExtension('chrome.debugger.attach', [{ tabId }, '1.3']);
    const result = await this._sendToExtension('chrome.debugger.sendCommand', [
      { tabId },
      'Target.getTargetInfo',
    ]);

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Recover by re-attaching the target (navigate/reattach the tab) so a fresh sessionId is issued.
  2. Confirm the extension connection is healthy and the target tab is still attached.
  3. Treat stale-session commands as non-fatal: drop the command and re-issue after re-attach rather than retrying the same id.
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await model.sendCommand(sessionId, method, params);
} catch (e) {
  if (/No tab found for sessionId/.test((e as Error).message)) {
    // session is gone — re-attach the target to obtain a fresh sessionId, then drop the command
    return; // or re-issue after re-attach
  }
  throw e;
}

Prevention

When it happens

Trigger: Playwright sends a CDP command on a sessionId that was never created (typo/foreign id), that belonged to a tab already detached/closed, or to a child session (worker/oopif) whose owning tab has been detached. The two-step lookup (`_findTabSession` by sessionId, then by `childSessions.has`) both fail.

Common situations: A child session (service worker, out-of-process iframe) was destroyed but Playwright still holds its sessionId and sends a command; the extension detached a tab mid-operation; protocol version drift between Playwright and the extension producing session ids the relay didn't record.

Related errors


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