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

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!
Subscribe to my newsletter
Read articles from codanyks directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
