Client-Side WebSockets
🌐 Client-Side WebSockets
WebSocket API has no automatic reconnection. If the connection drops, it's gone, and building something better is entirely this chapter's job.
The Native API
The same four events from Chapter 3's server-side ws library, mirrored on the client: open, message, close, error. Sending is the same one method too — socket.send(...), guarded by the same readyState check from Chapter 3.
🔁 Building Reconnection By Hand
Since nothing does this automatically, a reconnecting client needs its own retry loop — and a naive one causes real problems:
Naive Retry vs Backoff + Jitter
Naive: Retry Immediately
If the server restarts and drops every connection at once, every client reconnects in the same instant — a thundering herd that can overwhelm the server right as it's coming back up.
Backoff + Jitter
Each retry waits longer than the last (backoff), and the exact delay is randomized a little (jitter) — spreading reconnection attempts out instead of all landing at once.
A Small Reconnecting Wrapper
🟢 Reflecting Connection State in the UI
Users need to know when the connection isn't live — a small state machine drives that:
| State | UI Treatment |
|---|---|
| Connecting | Neutral indicator ("Connecting...") |
| Connected | Normal UI, indicator hidden or green |
| Reconnecting | Visible warning ("Reconnecting..."), messages queued or disabled |
| Disconnected (gave up) | Clear error state, manual retry option |
What to Do With Messages Sent While Disconnected
Either queue them to send once reconnected, or reject them immediately with clear UI feedback — silently dropping a message the user thinks was sent is the worst option.
Cleaning Up
Close the socket explicitly on page unload or when a component unmounts in a single-page app — an abandoned open socket with live listeners is a real memory/connection leak.
💻 Coding Challenges
Challenge 1: Add a Maximum Retry Count
Extend the ReconnectingSocket class above so that after 10 failed reconnection attempts in a row, it stops retrying and instead exposes a way for the UI to detect the connection has been permanently given up on.
Goal: Practice turning an infinite retry loop into one with a defined, observable end state.
Challenge 2: Queue Messages While Disconnected
Add a send(data) method to ReconnectingSocket that queues messages if the socket isn't currently OPEN, and flushes the queue the moment the connection re-opens.
Goal: Practice handling the "user sends something while disconnected" case discussed in this chapter.
Challenge 3: Explain a Duplicate-Handler Bug
A developer's reconnection code calls this.connect() on every retry but never removes the event listeners attached to the previous (now-closed) socket. Explain what goes wrong as reconnections accumulate, and how to fix it.
Goal: Practice reasoning about listener lifecycle across multiple socket instances.
Every reconnect in the pattern above creates a brand-new WebSocket object — a completely fresh instance, not the old one somehow reopened. If code elsewhere holds a reference to the old socket (say, a variable captured before the first disconnect) and keeps calling .send() on it, those calls fail silently or throw, because that specific object is permanently closed — the "live" connection is a different object entirely now. Always read the current socket through the wrapper (e.g. this.socket) rather than caching a raw reference to one particular WebSocket instance.
🎯 What's Next
With a resilient client in place, the next chapter goes back to the server to build out what a real application actually needs: Broadcasting, Rooms & Presence — grouping connections, targeted messaging, and tracking who's currently online.