Deadlocks classically need four conditions: mutual exclusion (resources that can be held by only one party), hold-and-wait (a holder asks for another resource without releasing the first), no preemption, and a circular wait. Break any one and the deadlock is gone.
In practice the cause is almost always lock-ordering inconsistency: function A acquires mu1 then mu2, function B acquires mu2 then mu1. As soon as both run concurrently, deadlock. The fix is a global lock-acquisition order — a lock hierarchy — that every site honors. Static analysis can sometimes catch this; convention always can.
Database deadlocks are a special case. Postgres detects them and returns an error that your code must retry; MySQL does the same. The right architectural answer is shorter transactions with consistent ordering of touched rows, plus a back-off + retry on 40P01 / 1213.
Review heuristic
Any function that acquires more than one lock, holds onto the first while waiting for the second, and isn't ordered consistently with every other function that touches those locks — that's a deadlock waiting for the right two requests to arrive together.
External reference
CWE-833: Deadlock — the canonical industry classification for this bug class. Useful when filing tickets, writing security policies, or arguing with a static analyzer.