100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Web Development

Close Codes and Error Handling

What WebSocket close codes mean, how to choose the right one, and how to build robust reconnect and error-handling logic around them.

The ProtocolAdvanced10 min readJul 10, 2026
Analogies

The Standard Close Code Ranges

Every WebSocket close frame can carry a 2-byte unsigned integer status code, and the specification reserves specific ranges with specific meanings: 1000-2999 are reserved for the protocol itself (1000 is normal closure, 1001 going away, 1002 protocol error, 1003 unsupported data, 1007 invalid payload data, 1008 policy violation, 1009 message too big, 1011 internal server error), 3000-3999 are reserved for use by libraries, frameworks, and registered extensions, and 4000-4999 are left entirely open for private application use between a client and server that both understand the same custom scheme. Codes 1004, 1005, 1006, and 1015 are reserved but must never actually appear on the wire — they exist only as internal signal values a local implementation reports when it detects specific failure conditions.

🏏

Cricket analogy: It's like the ICC's playing conditions rulebook reserving specific clause numbers for universally recognized dismissals (bowled, caught, LBW) while leaving other numbers open for a domestic board's own local match regulations that only apply within that competition.

Choosing the Right Close Code

Picking an accurate close code matters because client code, monitoring dashboards, and reconnect logic all branch on it. A server should send 1000 with a clear reason string only for genuinely intentional, successful completions, like a game session ending normally; it should use 1008 (policy violation) when rejecting a client for something like a failed auth check or rate-limit breach, 1011 when an unexpected server-side exception occurred that the client shouldn't necessarily retry immediately, and 1003 or 1007 when the client sent data the server structurally cannot process, such as binary data on an endpoint that only accepts JSON text or a text frame with invalid UTF-8. Overusing the generic 1000 for every case, including actual failures, is a common mistake that makes debugging production issues much harder because client-side logs lose the distinction between 'this ended on purpose' and 'this ended because something broke.'

🏏

Cricket analogy: It's like a scorer correctly recording the exact mode of dismissal — bowled, run out, or retired hurt — rather than lumping every wicket under a generic 'out,' because the specific reason matters enormously for later analysis of a bowler's or batter's record.

javascript
// Client-side reconnect logic that respects the close code
function connect(url) {
  const socket = new WebSocket(url);

  socket.addEventListener('close', (event) => {
    console.log(`Closed: code=${event.code} reason=${event.reason}`);

    if (event.code === 1000 || event.code === 1001) {
      // Normal closure or server going away intentionally: don't auto-reconnect
      return;
    }
    if (event.code === 1008 || event.code === 4001) {
      // Policy violation / custom auth failure: don't retry, surface to the user
      showAuthError();
      return;
    }
    // Everything else (1006, 1011, network drop) is worth retrying with backoff
    scheduleReconnect(url);
  });

  return socket;
}

let attempt = 0;
function scheduleReconnect(url) {
  const delay = Math.min(30000, 1000 * 2 ** attempt++);
  setTimeout(() => connect(url), delay);
}

Building Resilient Reconnect Logic

Robust WebSocket clients treat disconnection as a normal, expected event rather than an exception, and implement exponential backoff with jitter for reconnect attempts rather than retrying instantly in a tight loop, which can hammer a struggling server right when it can least handle the load or trigger a client-side ban from a rate limiter. Beyond just reopening the socket, resilient logic needs to resynchronize application state after a gap — for example, requesting any messages missed since the last known sequence number or timestamp — because a reconnect only re-establishes the transport; it does nothing to recover data that would have arrived during the outage unless the application layer explicitly accounts for it.

🏏

Cricket analogy: It's like a team returning to the field after a rain delay not simply resuming as if no time passed, but the umpires recalculating a revised target using the Duckworth-Lewis-Stern method to account for what was actually lost during the interruption.

Exponential backoff with jitter typically looks like delay = min(maxDelay, baseDelay * 2^attempt) plus a small random offset, which prevents a large fleet of clients from all retrying in synchronized bursts (a 'thundering herd') after a shared outage, such as a load balancer restart.

  • Close codes 1000-2999 are protocol-reserved, 3000-3999 for libraries/extensions, and 4000-4999 are free for private application use.
  • Codes 1004, 1005, 1006, and 1015 are reserved but must never actually appear on the wire; they're local-only signals.
  • Use 1000 only for genuinely successful, intentional closures — overusing it for failures hides real problems.
  • 1008 (policy violation) suits auth/rate-limit rejections; 1011 suits unexpected server errors; 1003/1007 suit malformed data.
  • Resilient clients treat disconnection as normal and use exponential backoff with jitter rather than tight-loop retries.
  • Reconnecting only restores the transport — applications must explicitly resynchronize any state or messages missed during the gap.
  • Branching reconnect behavior on the close code (e.g., not retrying after 1008) avoids wasted retries against permanent failures.

Practice what you learned

Was this page helpful?

Topics covered

#WebDevelopment#WebSocketsStudyNotes#CloseCodesAndErrorHandling#Close#Codes#Error#Handling#ErrorHandling#StudyNotes#SkillVeris