A browser WebSocket fires an error event with no details and then a close event with code 1006, an empty reason and wasClean false. What does that combination tell you about how the connection ended, and why does the API give you nothing more?
answer
- one code the wire never carries
- no close frame means no reason
- wasClean tells you the same story
- error event is empty by design
- network probing is the reason why
basics
~20 sCode 1006 means the connection ended without a close handshake, so the browser synthesised the code locally rather than receiving one. The error event is deliberately detail-free for security, so 1006 alone cannot distinguish a rejected connection from a dropped network link.
solid answer
~50 s1006 is a reserved code that never travels on the wire. The browser assigns it whenever it has to fail the connection abnormally — a refused or reset TCP connection, a server that answered the upgrade request with an ordinary HTTP response, a TLS failure, a proxy killing an idle connection, or a peer that simply vanished. `wasClean: false` says the same thing from another angle, and `reason` is empty because a reason string can only come from a peer's close frame, which never arrived. The preceding `error` event is a plain `Event` with no code and no message on purpose: telling a script *why* a cross-origin connection failed would let any page probe hosts and ports on the user's network. The practical consequences are that 1006 is your "retry with backoff" signal, and that if you need the client to know *why* it was shut out, the server has to open the connection first and then close cleanly with an application code in the 4000–4999 range.
go deeper
Recognise the CloseEvent fields — code, reason, wasClean — and know that 1006 means the connection ended abnormally rather than being an error the server chose to report.
Explain that 1005, 1006 and 1015 are reserved and never sent on the wire, that close() only accepts 1000 or 3000–4999, and why the error event carries no detail.
Show how you build diagnosis around an opaque failure: log the code, reason and connection duration, separate fast 1006s from idle-timeout 1006s, and design the server to reject clients with a clean application close code instead.
Own the contract between client and server for shutdown. Decide what your 4000-range codes mean, which are permanent and which are retryable, and make that table part of the protocol spec so every client implementation reconnects consistently.
## Clean close versus abnormal close A well-behaved shutdown is a handshake: one side sends a close frame carrying a status code and an optional reason, the other side echoes it, and both then drop the underlying connection. When that happens, the browser fires only a `close` event, and the `CloseEvent` carries `wasClean: true` along with the code and reason the peer actually sent. When the connection dies without that exchange, the browser instead *fails the connection*: it fires an `error` event and then a `close` event with `wasClean: false`. Since no close frame arrived, there is no code to report — so the specification reserves 1006 to mean exactly "the connection was closed abnormally, with no close frame received". Seeing 1006 is therefore not information from the server; it is the browser telling you it has nothing from the server. ## The reserved codes you will never see on the wire Three status codes exist purely as local signals and must never appear in an actual close frame: - **1005** — "no status received". A close frame arrived but carried no code, which happens when a peer calls `close()` with no arguments. This one is still a *clean* close: `wasClean` is true. - **1006** — "abnormal closure". No close frame at all. - **1015** — reserved for a TLS handshake failure. Everything else you are likely to handle does travel: 1000 normal closure, 1001 going away (the endpoint is shutting down or a page navigated away), 1002 protocol error, 1003 unsupported data, 1008 policy violation, 1009 message too big, 1011 an unexpected server-side error. Codes 3000–3999 are for libraries and frameworks registered with IANA, and 4000–4999 are free for your own application to define. ```js socket.addEventListener('close', (event) => { if (event.code === 1000) return; // we are done, do not reconnect if (event.code >= 4000 && event.code < 5000) { // our own protocol spoke handleApplicationClose(event.code, event.reason); return; } scheduleReconnect(); // 1006 and friends: try again }); ``` ## Why close() rejects 1006 Because 1006 is a local-only signal, `socket.close(1006, 'lost')` throws an `InvalidAccessError`. `close()` accepts 1000 or any code in 3000–4999 and nothing else, and the optional `reason` must fit in 123 bytes of UTF-8 or it throws a `SyntaxError`. Candidates who have only ever read close codes are often surprised that the set you may *send* is much narrower than the set you may *receive*. ## The deliberately empty error event The `error` event handed to `onerror` is a bare `Event`. It has no `code`, no `message`, no `reason`. This is not an oversight and it is not something a newer API level will fix. A page can open a WebSocket to any host, including one on the user's own local network. If the failure told scripts apart — "connection refused" versus "host unreachable" versus "rejected" — a page could scan that network and fingerprint what is running on it. The browser also declines to expose the HTTP status of a rejected upgrade for the same reason. So the error event exists to say "something went wrong", nothing more, and the diagnosis lives outside JavaScript: the browser's devtools network panel, which does show you the failed request, and the server's own logs. ## What this forces on your design Two things follow directly. First, **all your client-side reconnect logic has to key off close codes, not error details.** 1006 is ambiguous by construction, so it maps to "transient, retry with capped backoff and jitter". A clean 1000 that you initiated maps to "stop". Anything in your own 4000-range maps to whatever you defined. Second, **if a client needs to know why it was refused — bad credentials, unsupported version, account suspended — the server cannot signal that by refusing the connection.** It must accept the connection and then close it cleanly with an application code and a short reason, or send an error message over the open socket before closing. Teams routinely discover this the hard way when a rejected connection produces an indistinguishable 1006 in production and the client retries forever against a server that will never accept it. A useful diagnostic habit: log `code`, `reason`, `wasClean` and how long the socket stayed open. A 1006 arriving milliseconds after construction points at a refused connection or a rejected upgrade; a 1006 after a long quiet period points at an idle timeout in a proxy or load balancer, which is exactly the case an application heartbeat is meant to catch.
- How can a server tell a browser client that its credentials were rejected, given that a refused connection surfaces as 1006?It cannot signal it by refusing. The server should accept the connection, then either send an application error message or close cleanly with a code it has defined in the 4000–4999 range plus a short reason. The client sees a clean close with a meaningful code and can stop retrying instead of hammering a server that will never let it in.
- What is the difference between close code 1005 and 1006?1005 means a close frame arrived but carried no status code — a peer called `close()` with no arguments — and it is a clean close, so `wasClean` is true. 1006 means no close frame arrived at all and the browser synthesised the code; `wasClean` is false. Both are reserved and can never be sent on the wire.
- Your client sees 1006 only after several quiet minutes, never during active traffic. What does that pattern suggest?An idle timeout somewhere in the path — a proxy, load balancer or NAT device dropping a connection with no traffic on it. The socket may even sit at readyState OPEN until the next send fails. An application-level heartbeat on an interval below that timeout both keeps the path warm and detects a half-open connection quickly.
saying these in an interview costs you the question
- Thinks the server chose to send close code 1006
- Expects the error event to carry a status or message
- Tries to call close(1006) to signal a dropped link
- Treats wasClean false as meaning the data was corrupted
- Assumes a rejected connection is distinguishable from a network failure