Glossary / Deadlocks in async workflows

Deadlocks in async workflows

A deadlock is a state where a set of operations cannot make progress because each waits for a condition that another blocked operation must produce. JavaScript's single-threaded event loop avoids some traditional thread-lock patterns, but async workflows can still deadlock through promises, semaphores, queues, worker protocols, database locks, or resource pools.

Common async deadlock shapes

A task can hold a semaphore permit while awaiting another task that needs the same exhausted semaphore. Two services can each await a callback/message from the other while neither sends the next required message. A stream producer may await consumer progress while the consumer waits for producer completion before reading. A worker can block on Atomics.wait while the only agent able to notify it is itself waiting. Database transactions can acquire locks in opposing orders.

await semaphore.acquire();
try {
  await operationThatAlsoNeedsSemaphore(); // can wait forever at limit 1
} finally {
  semaphore.release();
}

This example deadlocks when operationThatAlsoNeedsSemaphore cannot proceed without another permit and the outer scope retains the only permit. A timeout can expose or bound waiting but does not itself restore invariants or cancel underlying work.

Prevention and diagnosis

Define resource ordering and acquire multiple locks in a consistent global order. Avoid holding scarce permits or transactions across unrelated awaits. Keep request/response protocols explicit, with correlation IDs, terminal error messages, cancellation, and timeouts. Ensure every wait has a possible notifier and every queue has a consumer path. Bound waits and instrument wait duration, queue depth, held-resource ownership, and operation IDs.

A dependency graph helps: nodes are tasks or resources, edges represent “waits for” or “holds”. A cycle is a deadlock candidate; verify that no external event can break it. Distinguish deadlock from starvation (some work progresses while one task waits indefinitely), slow I/O, and a leaked promise with no completion path.

Interview questions

  1. Can JavaScript have deadlocks without multiple JS threads? Yes; promises and resource protocols can form cyclic waits even on one event loop.
  2. Why can a semaphore deadlock? A task may hold a permit while awaiting nested work that needs the exhausted permit.
  3. Does a timeout solve a deadlock? It bounds waiting only if timeout handling cancels or safely unwinds the blocked operation.
  4. How do consistent lock ordering and short critical sections help? They reduce cyclic acquisition and avoid holding scarce resources across unrelated waits.
  5. Deadlock vs starvation? Deadlock means the involved dependency cycle cannot progress; starvation means a task is indefinitely denied while other work may continue.

References