In this lesson, you will learn how to prevent data corruption by implementing locking mechanisms to ensure atomic updates when multiple WebSocket events interact with the same shared resource.
The Problem: Asynchronous Interference
In a Node.js WebSocket server, events are processed asynchronously. Imagine you have a shared wallet object. A user sends two rapid "deposit" messages. If your code performs a "read-modify-write" operation—where it fetches the balance, adds the amount, and saves it back—an incoming event might trigger the second update before the first has finished saving. This leads to a race condition where one deposit is overwritten and lost.
The Solution: Using an Async Mutex
To maintain consistency, we must ensure that these operations are atomic. Since Node.js is single-threaded but non-blocking, we can use a "mutex" (mutual exclusion) lock to queue operations. When a process needs to update a resource, it must "acquire" the lock. Any subsequent requests must wait until the lock is released.
Here is how you might implement a simple locking pattern:
const locks = new Map();
async function updateWallet(userId, amount) {
// Ensure we have a lock for this specific user
if (!locks.has(userId)) {
locks.set(userId, Promise.resolve());
}
// Queue the operation behind the existing promise chain
const previousOp = locks.get(userId);
const currentOp = previousOp.then(async () => {
const balance = await db.getBalance(userId);
await db.setBalance(userId, balance + amount);
});
locks.set(userId, currentOp);
return currentOp;
}
By chaining the promises, you force the operations to execute sequentially rather than concurrently. Even if the WebSocket handler receives five messages in a few milliseconds, the currentOp ensures the database read and write happen in a predictable order for that specific user.
While this approach works for memory-resident state or single-server instances, remember that if you move to a multi-server setup, you would eventually need to transition from an in-memory lock to a distributed locking mechanism, such as one provided by Redis.
Key Takeaway: Race conditions in WebSocket handlers occur when multiple events access shared state concurrently; using promise-based locking ensures that sensitive operations complete sequentially, preserving data integrity.