Sending and Receiving
Once a connection is OPEN, the day-to-day work is just two things: pushing messages out with send() and reacting to messages coming in through the message event. This lesson nails down both directions, plus the two rules that trip people up most — when you may send, and what order things arrive.
Sending with ws.send()
Section titled “Sending with ws.send()”To send, you call send() with your payload:
ws.send('hello, server');ws.send(JSON.stringify({ type: 'chat', text: 'hi' }));send() accepts a string or binary data (we cover binary in the next lesson). It does not return a value and gives you no acknowledgement — it simply hands the message to the connection. A common pattern is to send structured data by serializing an object to JSON, then parsing it on the other end.
Receiving via the message event
Section titled “Receiving via the message event”Incoming messages arrive as message events, and the payload is always on event.data:
ws.onmessage = (event) => { const data = event.data; // a string (or binary, next lesson) const msg = JSON.parse(data); // if the peer sent JSON console.log(msg.type, msg.text);};event.data is the only place the payload lives. If you sent JSON, you parse it here; if you sent plain text, you use it directly. The handler runs once per message and may fire as often as the server has things to say.
You can only send when OPEN
Section titled “You can only send when OPEN”This is the single rule worth tattooing on your memory: send() is only valid while readyState is OPEN.
- Call
send()while stillCONNECTINGand it throws anInvalidStateError— the connection is not ready. - Call
send()afterCLOSINGorCLOSEDand the data is silently dropped; it never reaches the peer.
So the safe pattern is to send from inside onopen, or to guard every call:
function safeSend(ws: WebSocket, data: string) { if (ws.readyState === WebSocket.OPEN) { ws.send(data); } else { console.warn('not open — dropping or queuing message'); }}Ordering is guaranteed
Section titled “Ordering is guaranteed”A WebSocket runs over a single TCP connection, and that buys you a strong, simple guarantee: messages are delivered in the order you sent them. If you call send('a') then send('b') then send('c'), the peer receives a, b, c — never reordered, never duplicated. The same holds for messages the server sends to you.
sequenceDiagram
participant App as Your code
participant WS as WebSocket
participant Srv as Server
Note over WS: readyState = OPEN
App->>WS: send("a")
App->>WS: send("b")
WS->>Srv: a
WS->>Srv: b
Srv-->>WS: reply to a
WS-->>App: message event (data = reply to a)
Srv-->>WS: reply to b
WS-->>App: message event (data = reply to b) What ordering does not promise is a request/response pairing. A WebSocket has no built-in notion of “this reply belongs to that send” — if you need that, you add your own correlation id inside the message. Ordering, yes; matching, that is your job.
Send and receive, live
Section titled “Send and receive, live”The demo uses an in-page echo socket with the real WebSocket API, so it runs anywhere with no server. It sends three messages in a row from inside onopen and prints each echo as it returns — in order. In a real app only the first line would change to new WebSocket('wss://your-server').
Notice the first attempt — too early — is blocked because the socket is still CONNECTING. Then, once open, the three numbered messages come back as 1, 2, 3, in exactly the order they were sent. That is send(), the message event, the OPEN-only rule, and ordering, all in one run.
Common mistakes
Section titled “Common mistakes”Parsing JSON on every message with no error handling Symptoms:
JSON.parse(event.data)with no try/catch- The server sends malformed JSON and the app crashes
- Wrap it:
try { const msg = JSON.parse(event.data); } catch (e) { console.error('Invalid message', e) }
Sending large messages in a loop without checking bufferedAmount Symptoms:
- A loop sends 1,000 frames back-to-back — the buffer overflows
ws.bufferedAmountclimbs very high — a memory spike- Check
ws.bufferedAmount < thresholdbefore sending the next message
💡 Real-world example
The general pattern chat apps use (the same shape Slack’s legacy RTM API followed):
- Receive messages as JSON, e.g.
{ "type": "message", "text": "Hello", "channel": "C1234" }, and dispatch on thetypefield — event-driven message handlingGoogle Docs:
- Uses Operational Transformation (OT) — sends operations to the server and applies the updates it gets back to the local document state (Wikipedia — Operational transformation)