SeleniumHQ/selenium · error · Error

BiDi connection is closed

Error message

BiDi connection is closed

What it means

Thrown by `BiDi.send()` (and `waitForConnection()`) when `this._closed` is true — i.e. the connection has been permanently closed. The `_closed` flag is set by `_failPending()`, which is triggered by `close()`, the WebSocket 'close' event (peer/server disconnected), or the 'error' event. Once closed, every subsequent BiDi command throws this synchronously; it is terminal for that connection instance.

Source

Thrown at javascript/selenium-webdriver/bidi/index.js:205

      if (this.connected) {
        resolve()
        return
      }
      // Park the waiter in a Set so the constructor's 'open' handler can
      // resolve it and _failPending() can reject it. Avoids attaching socket
      // listeners that close()'s removeAllListeners('close') would strip.
      this._connectWaiters.add({ resolve, reject })
    })
  }

  /**
   * Sends a bidi request
   * @param params
   * @returns {Promise<unknown>}
   */
  async send(params) {
    if (this._closed) {
      throw new Error('BiDi connection is closed')
    }
    if (!this.connected) {
      await this.waitForConnection()
    }
    // Defense in depth: even after waitForConnection() resolves, the socket
    // may have transitioned to CLOSING/CLOSED (e.g. caller closed the raw
    // socket). Refuse rather than throwing from inside ws.send().
    if (this._ws.readyState !== WebSocket.OPEN) {
      throw new Error('BiDi connection is not open')
    }

    const id = ++this.id

    this._ws.send(JSON.stringify({ id, ...params }))

    return new Promise((resolve, reject) => {
      const timeoutId = setTimeout(() => {
        this._pending.delete(id)

View on GitHub (pinned to aa36b38e69)

Solutions

  1. Do not issue BiDi commands after driver.quit()/bidi.close() — tear down listeners first
  2. Re-acquire the BiDi connection via driver.getBidi() after a reconnect/restart if the session is still alive
  3. For event-driven code, guard with the connection state before sending
  4. Catch the error and stop/skip the operation rather than blindly retrying on a dead connection

Example fix

// before
driver.quit()
await bidi.send({ method: 'session.status', params: {} }) // throws: connection is closed

// after — stop BiDi work before quitting
await networkInspector.clearListeners()
driver.quit()
Defensive patterns

Strategy: try-catch

Validate before calling

if (bidi.isConnected && bidi.socket && bidi.socket.readyState === 1 /* OPEN */) {
  await bidi.send(cmd)
} else {
  // connection gone — reconnect via driver.getBidi() or skip
}

Type guard

const isBiDiOpen = (bidi) => bidi.isConnected && bidi.socket?.readyState === 1

Try / catch

try {
  await bidi.send(cmd)
} catch (e) {
  if (/connection is closed/i.test(e.message)) {
    // session ended — stop BiDi work, do not retry blindly
    return
  }
  throw e
}

Prevention

When it happens

Trigger: Calling any BiDi method after `driver.quit()` or after the WebSocket dropped; reusing a BiDi handle across a navigation that severed the socket; calling send after an explicit `bidi.close()`; an async event handler issuing a command after teardown.

Common situations: Test teardown calls `driver.quit()` but an async network/log listener still issues a BiDi command; flaky CI where the Grid drops the WebSocket; commands issued after a tab/window close severed the BiDi session.

Related errors


AI-assisted analysis of SeleniumHQ/selenium@aa36b38e69 (2026-08-14). Data as JSON: /api/errors/463a0233c9de7e27. Report an issue: GitHub.