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
- Do not issue BiDi commands after driver.quit()/bidi.close() — tear down listeners first
- Re-acquire the BiDi connection via driver.getBidi() after a reconnect/restart if the session is still alive
- For event-driven code, guard with the connection state before sending
- 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
- Tear down all BiDi listeners/interceptors before driver.quit()
- Treat 'connection is closed' as terminal — reconnect via driver.getBidi() if the session is alive, otherwise stop
- Track session lifecycle so async handlers don't outlive the connection
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
- BiDi connection is not open
- timeout must be a positive number
- interval must be a positive number
- Timed out waiting for response to BiDi command {current_id}
- #{message['error']}: #{message['message']} #{message['stackt
AI-assisted analysis of SeleniumHQ/selenium@aa36b38e69 (2026-08-14).
Data as JSON: /api/errors/463a0233c9de7e27.
Report an issue: GitHub.