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