In the browser you write `const ws = new WebSocket(url); ws.send('hello');` on the very next line. What happens, and what does the WebSocket readyState tell you about when send() is legal?
answer
- the constructor returns before the connection exists
- readyState has four numbered values
- one state throws, two states swallow
- open event is the green light
- CONNECTING send throws InvalidStateError
basics
~10 sThe send() call throws an InvalidStateError, because the socket is still in the CONNECTING state — the constructor only starts the connection. Send once the open event has fired and readyState is WebSocket.OPEN.
solid answer
~40 s`new WebSocket(url)` returns immediately with `readyState === WebSocket.CONNECTING` (0); the connection is still being established in the background. Calling `send()` in that state throws an `InvalidStateError` DOMException — the API deliberately refuses to queue data for a socket that may never open. The legal window is `OPEN` (1), which you learn about from the `open` event, so real code either sends inside the `open` handler or pushes into its own outbox array and flushes it there. The asymmetry worth memorising: once the socket reaches `CLOSING` (2) or `CLOSED` (3), `send()` does **not** throw — the data is simply discarded, so a fire-and-forget send after a dropped connection fails invisibly. A `if (ws.readyState === WebSocket.OPEN)` guard covers both ends of that range.
code
javascript · 20 linesconst ws = new WebSocket('wss://example.com/feed');
const outbox = [];
function publish(message) {
const payload = JSON.stringify(message);
if (ws.readyState === WebSocket.OPEN) {
ws.send(payload);
} else if (ws.readyState === WebSocket.CONNECTING) {
outbox.push(payload);
}
// CLOSING or CLOSED: this socket is finished, drop or hand to a reconnect layer
}
ws.addEventListener('open', () => {
while (outbox.length > 0) {
ws.send(outbox.shift());
}
});
publish({ type: 'subscribe', channel: 'prices' });go deeper
Know that the constructor only starts the connection and that the first send() belongs inside the open handler. Be able to name the four events: open, message, error, close.
Explain the four readyState values by number and the asymmetry between them: CONNECTING makes send() throw InvalidStateError, while CLOSING and CLOSED make it silently discard the data.
Show how you make send() safe for arbitrary callers — a bounded outbox flushed on open, a readyState guard, and an application-level acknowledgement scheme, since the platform reports nothing about dropped sends.
Own the question of what delivery guarantee your realtime layer actually offers. The socket gives at-most-once with no feedback, so decide where sequencing, replay and idempotency live before teams build product features on top of it.
## A synchronous constructor over an asynchronous connection `new WebSocket('wss://example.com/feed')` hands you a fully formed object on the very next line, but nothing has been established yet. The constructor's only job is to validate the URL and kick off the connection attempt; everything after that is reported through events. This is the single most common source of confusion with the API, because the object *looks* usable the instant you have it. The URL must use `ws:` or `wss:` (the constructor also accepts `http:`/`https:` forms and normalises them) and must not contain a fragment; a malformed URL throws a `SyntaxError` right there in the constructor, synchronously. That is a different failure from a connection that cannot be established, which arrives later as an `error` event followed by a `close` event. ## The four ready states `ws.readyState` is a number, and the interface exposes named constants for it: ```js WebSocket.CONNECTING // 0 — constructor has run, connection not yet established WebSocket.OPEN // 1 — open event has fired, send() is legal WebSocket.CLOSING // 2 — close() called, or a close is in progress WebSocket.CLOSED // 3 — connection is finished, or never succeeded ``` The progression is one-way. A `WebSocket` instance is single-use: once it reaches `CLOSED` it can never reopen, and reconnecting means constructing a brand-new object. There is no `ws.reopen()`. ## Why CONNECTING throws but CLOSED does not The specification defines exactly one throwing case for `send()`: if the ready state is `CONNECTING`, throw an `InvalidStateError` DOMException. The reasoning is that a not-yet-open socket might still fail, and silently holding your bytes would give you a false promise of delivery. Once the socket is `CLOSING` or `CLOSED`, `send()` is a no-op — no exception, no return value, no event. Nothing tells the caller the message evaporated. That asymmetry catches people out: they wrap the first send in a `try/catch`, see it work, and assume the API will keep complaining if something is wrong. It will not. Detecting *lost* messages is entirely your problem, which is why real-time protocols built on WebSockets carry their own sequence numbers or acknowledgements. ## The four events A `WebSocket` is an `EventTarget`, so both the `on*` properties and `addEventListener` work: ```js ws.addEventListener('open', () => { /* readyState is now OPEN */ }); ws.addEventListener('message', (e) => { /* e.data is a string, Blob, or ArrayBuffer */ }); ws.addEventListener('error', () => { /* a plain Event with no detail */ }); ws.addEventListener('close', (e) => { /* CloseEvent: e.code, e.reason, e.wasClean */ }); ``` `addEventListener` is worth preferring over `ws.onopen = …` when more than one part of your code cares about the socket, since the property form allows only one handler and a second assignment silently replaces the first. It also lets you use `{ once: true }` and `{ signal }` for teardown. Ordering guarantees are simple: `open` fires at most once; `close` always fires last; an abnormal ending fires `error` immediately before `close`, while a clean shutdown fires only `close`. ## An outbox makes send() safe to call anywhere Rather than forcing every caller to know the socket's state, most codebases wrap it: ```js const outbox = []; function publish(message) { const payload = JSON.stringify(message); if (ws.readyState === WebSocket.OPEN) ws.send(payload); else outbox.push(payload); } ws.addEventListener('open', () => { while (outbox.length) ws.send(outbox.shift()); }); ``` This handles both the startup race (a `publish()` call that happens during `CONNECTING`) and the reconnect case, provided you also bound the outbox — an unbounded array in front of a socket that never opens is just a memory leak with extra steps. ## What goes wrong in real code The bug rarely shows up locally, because a localhost handshake completes so fast that the first `send()` often lands after `open` by luck. On a real network with a hundred milliseconds of latency it fails every time. Reviewers should treat any `send()` that is not inside an `open` handler and not guarded by a `readyState` check as a race, not a style preference.
- If send() silently discards data once the socket has closed, how does the sending code ever find out something was lost?It does not — `send()` has no return value and fires no event on a closed socket. Detecting loss has to be built into your own message protocol: sequence numbers, application-level acknowledgements, or a resend-on-reconnect queue. The platform gives you nothing beyond `readyState` and the `close` event to reason with.
- Can you reuse a WebSocket object after it has closed?No. A `WebSocket` is single-use: the ready state only moves forward, and `CLOSED` is terminal. Reconnecting means constructing a new `WebSocket` and rewiring its four handlers. Any reconnect helper therefore has to manage object replacement, not just state, and must drop references to the dead socket so its listeners are collected.
- Why prefer addEventListener over assigning ws.onmessage?`WebSocket` extends `EventTarget`, so `addEventListener` supports multiple independent listeners, `{ once: true }`, and removal via an `AbortSignal` passed as `{ signal }`. Assigning `ws.onmessage` allows exactly one handler, and a second assignment elsewhere in the codebase silently replaces the first — a genuinely hard bug to spot.
saying these in an interview costs you the question
- Thinks new WebSocket() blocks until the connection is open
- Says send() before open just queues the message until later
- Assumes send() throws when the socket is already closed
- Treats readyState as a simple boolean connected flag
- Believes a closed WebSocket object can be reopened