Deadlock: break any one of four conditions
Mutual exclusion, hold and wait, no preemption, circular wait. All four must hold at once. The practical one to break is circular wait: give every lock a global order.
Deadlock is not a random bug. It is the guaranteed outcome of four conditions holding together. Once you see that, the fix becomes mechanical: break any one of them.
The four conditions
| Condition | Meaning |
|---|---|
| Mutual exclusion | a resource has one holder at a time |
| Hold and wait | holding one lock while requesting another |
| No preemption | a lock cannot be taken away |
| Circular wait | a cycle where A waits for B and B for A |
The first three are usually untouchable: exclusion is what a lock is, and preemption breaks the critical-section assumption. The fourth is the one you can change.
Break circular wait: impose an order
Give every lock a global order and acquire only in that order:
// each resource has a stable id
const order = (a: Account, b: Account) => (a.id < b.id ? [a, b] : [b, a]);
async function transfer(from: Account, to: Account, amount: number) {
const [first, second] = order(from, to);
await first.lock();
try {
await second.lock();
try {
// both locks are held in global order, so no cycle can form
first.balance -= amount;
second.balance += amount;
} finally { second.unlock(); }
} finally { first.unlock(); }
}
Transfers are the textbook case: without ordering, concurrent A-to-B and B-to-A is a guaranteed deadlock. With ordering, no cycle is possible.
Break hold and wait: take everything at once
Acquire all locks or none. The cost is lower concurrency, and it needs a way to grab several locks atomically.
Break no preemption: add a timeout
if (!await lock.tryAcquire({ timeoutMs: 500 })) {
// release what you hold and retry
}
This does not eliminate deadlock. It turns a permanent hang into a retryable failure. You need a fallback path, or you have only converted a deadlock into an error.
Detect instead of prevent
Databases generally take this route: allow deadlock, detect the cycle, and roll back one transaction. So production code must handle “transaction rolled back, retry”:
for (let attempt = 0; attempt < 3; attempt++) {
try { return await db.transaction(work); }
catch (e) { if (!isDeadlock(e) || attempt === 2) throw e; }
}
The most overlooked case
Implicit locks in async code. Awaiting a long or unbounded operation while holding a lock stretches the hold time without limit and turns a probabilistic deadlock into a certain one.
Order your locks. That single step resolves the vast majority of real deadlocks and needs no runtime support.

Comments
…