You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ws: once('error') emits a failed connection's error twice, and the second is an unhandled 'error' event #46222
With ws@8.21.0 installed (Bun uses its built-in ws regardless), run with bun repro.mjs:
import{WebSocket}from"ws";process.on("uncaughtException",err=>{console.log("uncaughtException:",err.message);});consterrors=[];// Nothing listens on port 1, so the connection is refused.constws=newWebSocket("ws://127.0.0.1:1");ws.once("error",err=>errors.push(err.message??String(err)));ws.on("close",code=>{setImmediate(()=>{console.log(JSON.stringify({onceErrorCalls: errors.length,close: code}));process.exit(0);});});
The same happens when the socket is closed before its handshake completes (new WebSocket(url) and then an immediate close()), which is what a UI component does when it unmounts while connecting.
Without the uncaughtException handler the process crashes.
Additional information
The native WebSocket dispatches error only once here (a persistent and a {once: true}addEventListener each see one event), so the duplicate comes from the ws shim (src/js/thirdparty/ws.js):
once("error", f) calls #onOrOnce("error", f, onceObject). No persistent listener is registered yet, so it adds a native error forwarder with {once: true}, and then calls super.once("error", f).
EventEmitter.prototype.once registers its wrapper through this.on(...). That is the shim's overridden on, so #onOrOnce("error", wrapper, undefined) runs. The persistent bit is still unset, so it adds a second, persistent native forwarder.
On failure both forwarders call this.emit("error", err). The first emit runs f, which removes the once wrapper. The second emit finds no error listener, and EventEmitter throws Unhandled error.
The EventEmitter.prototype.once → this.on step can be confirmed with a subclass that overrides on: calling once invokes it once, on both Bun and Node.
One fix: in #onOrOnce, register the EventEmitter side of a once listener without going through the overridden on. For example, add a self-removing wrapper with super.on (keeping wrapper.listener = listener so off(event, listener) still works), or let the native forwarder added in step 1 be the only one.
Related: #26358 (a later once("pong") no longer fires) is in the same once path but is a different symptom; its fix doesn't cover this.
Workaround: attach a persistent no-op error listener (ws.on("error", () => {})) before calling once("error", ...).
Activity
ykisana commentedon Oct 22, 2026
This should be fixed in 1.4.4, can do (
bun upgrade)Fixed here: PR #46528