Error Handling in WebSockets: Ensuring Stability in Real-Time Communication

codanykscodanyks
4 min read

Real-time apps are only as good as their stability. While WebSockets enable low-latency communication, they also introduce a set of challenges that need thoughtful error handling.

In this third article of our WebSocket series, we’ll explore strategies to build robust, fault-tolerant WebSocket systems using Node.js and the ws package.

"It’s not about being perfect. It’s about being resilient." – Adapted from Coach Carter

📖 Missed the last article? Securing WebSockets: Authentication in WebSocket Connections


🚨 Why Error Handling Matters

WebSockets are persistent, which means many things can go wrong:

  • Network issues

  • Client or server crashes

  • Message format problems

  • Token expiry or revoked access

If not handled gracefully, these issues can result in dropped connections, inconsistent state, or security gaps.


🧰 Common WebSocket Errors and How to Handle Them

Let’s break down the most frequent categories and how to deal with them.

1️⃣ Connection Failures

Client Side:

const ws = new WebSocket('ws://localhost:8080');

ws.addEventListener('error', (err) => {
  console.error('WebSocket error:', err);
});

ws.addEventListener('close', (e) => {
  console.log('Connection closed', e.reason);
  // Optional: Retry logic
  retryConnection();
});

function retryConnection() {
  setTimeout(() => {
    // Try reconnecting
  }, 2000);
}

Server Side:

wss.on('connection', (ws) => {
  ws.on('error', (err) => {
    console.error('Socket error:', err);
  });

  ws.on('close', (code, reason) => {
    console.log(`Socket closed: ${code} - ${reason}`);
  });
});

Tip: Use WebSocket close codes (e.g., 1000 for normal close, 1008 for unauthorized, 1011 for server error)


2️⃣ Invalid Messages

Parsing incoming data safely is a must:

ws.on('message', (msg) => {
  try {
    const parsed = JSON.parse(msg);
    // Process parsed message
  } catch (e) {
    ws.send(JSON.stringify({ error: 'Invalid JSON' }));
  }
});

3️⃣ Expired Tokens or Invalid Sessions

If using per-message auth:

ws.on('message', (msg) => {
  const { token, data } = JSON.parse(msg);

  if (!validateToken(token)) {
    ws.close(4001, 'Token expired or invalid');
    return;
  }

  // Process valid message
});

🔄 Retry and Reconnect Strategy

Here’s an improved retry strategy that:

  • Uses exponential backoff

  • Waits properly between retries

  • Automatically retries when the connection is unexpectedly closed or errors after being established

function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}

function setSocket(token) {
    return new Promise((resolve, reject) => {
        try {
            let isResolved = false;
            const ws = new WebSocket(`ws://localhost:8080`);

            ws.addEventListener("open", () => {
                console.log("Connected to server");
                const message = JSON.stringify({
                    token,
                    data: "Hello from native client!",
                });
                ws.send(message);
                isResolved = true;
                resolve(true);
            });

            ws.addEventListener("message", (event) => {
                try {
                    const parsed = JSON.parse(event.data);
                    console.log("Received from server:", parsed.message);
                } catch (e) {
                    ws.send(JSON.stringify({ error: "Invalid JSON" }));
                }
            });

            ws.addEventListener("error", () => {
                console.log("SetSocketError: WebSocket encountered an error.");
                if (isResolved) {
                    retryConnection(token);
                } else {
                    resolve(false); // Signal failure to trigger retry
                }
            });

            ws.addEventListener("close", () => {
                console.log("SetSocketClosed: Closed from server.", isResolved);
                if (isResolved) {
                    retryConnection(token);
                } else {
                    resolve(false); // Signal failure to trigger retry
                }
            });
        } catch (e) {
            console.log(
                "SetSocketException: Error connecting to server:",
                JSON.stringify(e)
            );
            resolve(false);
        }
    });
}

async function retryConnection(token, retries = 5) {
    let delay = 1000;

    async function attempt(retry) {
        console.log(`Attempt: ${retry}`);
        if (retry === 0) {
            console.log("Retry attempts exhausted.");
            return;
        }

        const connected = await setSocket(token);
        if (!connected) {
            console.log(`Retrying in ${delay} ms...`);
            await sleep(delay); // <-- await the delay
            delay *= 2; // Exponential backoff
            await attempt(retry - 1); // <-- await the next attempt
        }
    }

    await attempt(retries);
}

retryConnection(token);

Warning: Don’t retry infinitely. Use limits and user feedback.


🧪 Graceful Shutdown (Server)

process.on('SIGINT', () => {
  console.log('Shutting down...');
  wss.clients.forEach(client => client.close(1001, 'Server shutting down'));
  wss.close(() => process.exit(0));
});

✅ Summary

  • Always validate messages and handle parse errors

  • Handle connection loss and retry with backoff

  • Gracefully close sockets on server shutdown

  • Monitor close codes and emit meaningful errors

  • Validate auth tokens per message to prevent stale access

"Strong foundations make the strongest towers." – Unknown


This wraps up our WebSocket mini-series focused on native client support in Node.js 23. From getting connected to securing, and now stabilizing — you've built a solid real-time foundation.

💾 Sample Code on GitHub

Want to try it yourself? You can find the full example code used in this article in the GitHub repo:

👉 codanyks/native-websocket-nodejs

Until next time — happy hacking!

0
Subscribe to my newsletter

Read articles from codanyks directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

codanyks
codanyks