Rule
Release locks even on exception paths. 
Every lock acquisition must have a guaranteed release, even when exceptions occur. 
Supported languages:** Java, C, C++, PHP, JavaScript, TypeScript, Go, PythonIntroduction
Unreleased locks are one of the most common causes of deadlocks and system hangs in production Node.js applications. When an exception occurs between lock acquisition and release, the lock remains held indefinitely. Other async operations waiting for that lock hang forever, causing cascading failures across the system. A single unreleased mutex can bring down an entire API because the event loop becomes blocked and requests pile up. This happens with libraries like async-mutex, mutexify, or any manual lock implementation where release isn't automatic.
Why it matters
System stability and availability: Unreleased locks cause deadlocks that freeze async operations in Node.js. In Express or Fastify servers, this exhausts available workers, making the application unable to handle new requests. The only recovery is restarting the process, causing downtime. In microservices architectures, unreleased locks in one service can cascade failures across dependent services as they time out waiting for responses.
Performance degradation: Before complete deadlock, unreleased locks cause severe performance problems. Async operations contend for locked resources, creating a queue of pending promises that never resolve. Lock contention creates unpredictable latency spikes that degrade user experience. As concurrent request count increases under load, contention compounds exponentially.
Debugging complexity: Deadlocks from unreleased locks are notoriously difficult to debug in production Node.js apps. The symptoms appear far from the root cause, process hangs show pending promises but not which exception path failed to release the lock. Reproducing the exact sequence of exceptions that triggered the deadlock is often impossible in development environments.
Resource exhaustion: Beyond locks themselves, failure to release locks often correlates with failure to release other resources like database connections, Redis clients, or file handles. This compounds the problem, creating multiple resource leaks that bring systems down faster under load.
Code examples
❌ Non-compliant:
const { Mutex } = require('async-mutex');
const accountMutex = new Mutex();
async function transferFunds(from, to, amount) {
    await accountMutex.acquire();
    if (from.balance < amount) {
        throw new Error('Insufficient funds');
    }
    from.balance -= amount;
    to.balance += amount;
    accountMutex.release();
}
Why it's unsafe: If the insufficient funds error is thrown, accountMutex.release() never executes and the mutex remains locked forever. All subsequent calls to transferFunds() will hang waiting for the mutex, freezing the entire payment system.
✅ Compliant:
const { Mutex } = require('async-mutex');
const accountMutex = new Mutex();
async function transferFunds(from, to, amount) {
    const release = await accountMutex.acquire();
    try {
        if (from.balance < amount) {
            throw new Error('Insufficient funds');
        }
        
        from.balance -= amount;
        to.balance += amount;
    } catch (error) {
        logger.error('Transfer failed', { 
            fromId: from.id, 
            toId: to.id, 
            amount,
            error: error.message 
        });
        throw error;
    } finally {
        release();
    }
}Why it's safe: The catch block logs the error with context before re-throwing it, and the finally block guarantees the mutex release function executes whether the operation succeeds, throws an error, or the error is re-thrown from catch. The lock is always released, preventing deadlocks.
Conclusion
Lock release must be guaranteed, not conditional on successful execution. Use try-finally blocks in JavaScript or the runExclusive() helper provided by libraries like async-mutex. Every lock acquisition should have an unconditional release path visible in the same code block. Proper lock management is not optional, it's the difference between a stable system and one that randomly hangs under load.
.avif)
