All questions

Async Mutex

Premium

Async Mutex

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.

Signature

function asyncMutex(): {
  acquire(): Promise<() => void>; // resolves to a release function
  runExclusive<T>(fn: () => T | Promise<T>): Promise<T>;
  isLocked(): boolean;
};

Examples

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)

Notes

  • One holder at a time — while a caller holds the lock, every other acquire() stays pending until release() is called.
  • FIFO fairness — queued waiters are served in the order they 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.
  • No real timers — this is pure Promise plumbing; you never need setTimeout, and you must not busy-wait.

FAQ

Why do you need a mutex if JavaScript is single-threaded?
Single-threaded does not mean uninterruptible. Every await yields control back to the event loop, so two async operations can interleave and both read the same stale value before either writes it back. The mutex serializes them so each finishes before the next starts.
What are the time and space complexities?
acquire, release, and isLocked are all O(1) — a push or shift on the waiter queue plus a boolean flip. Space is O(n) for the n callers currently queued behind the holder.
Is this mutex reentrant?
No. If the flow that already holds the lock calls acquire again without releasing, it queues behind itself and waits forever — a deadlock. Release before re-acquiring, or build a reentrant variant that tracks the current owner and a nesting count.

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.

Upgrade to Premium