microsoft/playwright · error · Error

This tab is already connected to another client

Error message

This tab is already connected to another client

What it means

Thrown by the Playwright Chrome extension's MV3 background service worker when a connect request targets a browser tab that already belongs to another client's ConnectedTabGroup. The extension relays a tab's CDP traffic to one client at a time, so a second client (another MCP session, inspector, or connect page) cannot claim the same tab. The check at background.ts:106 fires when the chosen tab is not the selector page itself and its id already appears in _connectedTabIds().

Source

Thrown at packages/extension/src/background.ts:107

        });
        return false;
      case 'disconnect':
        this._connections.get(message.connectionId)?.close('User disconnected');
        sendResponse({ success: true });
        return false;
      case 'keepalive':
        // Connect page pings us every ~20s so receiving this message resets
        // the MV3 service worker idle timer and keeps the relay WebSocket alive.
        return false;
    }
  }

  private async _connectTab(selectorTabId: number, tab: chrome.tabs.Tab & { id: number }, clientName: string | undefined): Promise<void> {
    try {
      await this._cleanupPromise;
      this._releaseTab(selectorTabId);
      if (tab.id !== selectorTabId && this._connectedTabIds().has(tab.id))
        throw new Error('This tab is already connected to another client');

      const connection = await this._pendingConnections.take(selectorTabId);
      if (!connection)
        throw new Error('Pending client connection closed');

      const id = ++this._lastConnectionId;
      const taken = [...this._connections.values()].map(group => group.groupStyle);
      const group = new ConnectedTabGroup(connection, tab, clientName, uniqueGroupStyle(clientName, taken), tabId => this._pendingConnections.has(tabId));
      group.onclose = () => this._connections.delete(id);
      this._connections.set(id, group);

      await Promise.all([
        chrome.tabs.update(tab.id, { active: true }),
        chrome.windows.update(tab.windowId, { focused: true }),
      ]).catch(() => {});

      if (tab.id !== selectorTabId)
        await chrome.tabs.remove(selectorTabId).catch(() => {});

View on GitHub (pinned to 9642f57665)

Solutions

  1. Disconnect the existing client first: call the extension's 'disconnect' message with the connectionId from 'getConnectionStatus' (background.ts:82-93), or close the old connect page/client, then retry connecting.
  2. Connect to a different tab that is not yet in any group's connectedTabIds.
  3. If the owning client is gone but the group lingers, reload the extension (or wait for the MV3 service worker restart plus stale-group cleanup via cleanupStalePlaywrightGroups) so the old group is released.

Example fix

// before: second client selects a tab already owned by client A
await chrome.runtime.sendMessage({ type: 'connectToTab', tab: alreadyConnectedTab, clientName: 'client B' });
// -> Error: This tab is already connected to another client

// after: check status and disconnect the previous owner first
const { connections } = await chrome.runtime.sendMessage({ type: 'getConnectionStatus' });
const owner = connections.find((c: any) => c.connectedTabIds.includes(alreadyConnectedTab.id!));
if (owner)
  await chrome.runtime.sendMessage({ type: 'disconnect', connectionId: owner.id });
await chrome.runtime.sendMessage({ type: 'connectToTab', tab: alreadyConnectedTab, clientName: 'client B' });
Defensive patterns

Strategy: validation

Validate before calling

// Before sending 'connectToTab', ask the extension which tabs are taken
const { connections } = await chrome.runtime.sendMessage({ type: 'getConnectionStatus' });
const takenTabIds = new Set<number>(
  connections.flatMap((c: any) => c.connectedTabIds as number[]),
);
if (takenTabIds.has(targetTab.id!)) {
  const owner = connections.find((c: any) => c.connectedTabIds.includes(targetTab.id!));
  // either disconnect the owner or pick another tab
  await chrome.runtime.sendMessage({ type: 'disconnect', connectionId: owner.id });
}
await chrome.runtime.sendMessage({ type: 'connectToTab', tab: targetTab, clientName });

Try / catch

// The error arrives in the sendResponse payload, not as a thrown exception
const res = await chrome.runtime.sendMessage({ type: 'connectToTab', tab: targetTab, clientName });
if (!res.success && /already connected to another client/.test(res.error)) {
  // surface a 'disconnect previous client or choose another tab' action instead of retrying blindly
}

Prevention

When it happens

Trigger: A connect page (selector tab) sends the 'connectToTab' runtime message (background.ts:71-77) with a tab whose id is already inside another group returned by _connectedTabIds(). Concretely: two `playwright` MCP/CDP clients connect through the extension relay and both pick the same target tab; or a previous session's group is still open (not disconnected via 'disconnect', background.ts:91-93) while a new client selects that tab.

Common situations: Running two editors/agents (e.g. two MCP servers) that both use the extension against the same page; reconnecting after a client crash without disconnecting the old group; a stale connect page left open from an earlier run; selecting a tab that a still-active earlier client already claimed. Note the guard `await this._cleanupPromise` only reconciles groups orphaned by service-worker restarts, not live ones.

Related errors


AI-assisted analysis of microsoft/playwright@9642f57665 (2026-08-21). Data as JSON: /api/errors/473f18e39edd9638. Report an issue: GitHub.