A mutex (short for "mutual exclusion") is a lock that lets only one piece of code hold it at a time, so overlapping async operations take turns instead of running over each other. In single-threaded JavaScript you still need one: every await hands control back to the event loop, so two async functions can interleave and both read the same stale value before either writes it back. A mutex serializes them.
Implement asyncMutex(), which returns a mutex object with three methods. acquire() returns a Promise that resolves to a release function; only one caller may hold the lock at a time. If the lock is free, acquire() resolves right away; if it is held, the call waits in a FIFO queue and resolves when the current holder calls release(). runExclusive(fn) acquires the lock, awaits fn(), then releases — even if fn throws. isLocked() reports whether the lock is currently held.
function asyncMutex(): {
acquire(): Promise<() => void>; // resolves to a release function
runExclusive<T>(fn: () => T | Promise<T>): Promise<T>;
isLocked(): boolean;
};
const mutex = asyncMutex();
const release = await mutex.acquire(); // lock is free, so this resolves now
mutex.isLocked(); // true
release(); // hand the lock back
mutex.isLocked(); // false
// Two increments of a shared counter never interleave.
const mutex = asyncMutex();
let counter = 0;
const inc = () =>
mutex.runExclusive(async () => {
const read = counter; // read
await Promise.resolve(); // yield — pretend some async work happens here
counter = read + 1; // write
});
await Promise.all([inc(), inc()]);
counter; // 2 (without the mutex it would be 1)
acquire() stays pending until release() is called.acquire(), so no waiter is starved.release is idempotent — calling it twice does nothing the second time; it must never hand the lock to two waiters.runExclusive always releases — even when fn throws or rejects, the lock is freed before the error propagates.setTimeout, and you must not busy-wait.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.