I got E08252 NESTED_ENTER_DIFFERENT_LOCK — what does 'lock-order cycle' mean and how do I fix it?
← Concurrency · Ref: Q1298
E08252 fires when EK9's deadlock detector finds two or more places in your program that acquire the same set of MutexLocks in OPPOSITE orders, forming a cycle. At runtime, two threads running these paths concurrently can deadlock — each holds one lock and waits for the other.
WHAT THE ERROR MEANS
The analyzer builds a workspace-wide precedence graph: every time your code does `lockA.enter(...)` with `lockB.enter(...)` nested inside, an edge lockA → lockB is recorded. A CYCLE in this graph (e.g. lockA → lockB AND lockB → lockA somewhere) is a deadlock waiting to happen. The error message names the participating locks and renders the cycle path: 'lockA → lockB → lockA'.
NOT TRIGGERED BY SINGLE-DIRECTION NESTING (PROVABLY-ORDERABLE LOCKS ONLY)
If only ONE function nests lockB inside lockA (and no other code ever does the reverse), there is no cycle — that code is safe, PROVIDED the two locks are provably orderable: different types, or different field declarations. E08252 only fires when at least two places contradict each other.
IMPORTANT CAVEAT: this 'single direction is safe' rule does NOT extend to two locks of the SAME type on caller- or runtime-determined objects — two different same-type MutexLock parameters, or the same lock field on two different objects (the from.getLock()/to.getLock() bank transfer, two instances of one class, dining-philosophers forks). Those are interchangeable at the call site, so a single static direction can instantiate as both orders at runtime; EK9 rejects them even with no reverse site — that is E08255 (UNPROVABLE_LOCK_ORDER), not E08252. The fix is the same: one lock over an owning record.
THREE WAYS TO FIX
1. UNIFY UNDER ONE LOCK
If both resources genuinely need atomic protection together, put them in a record and guard with ONE MutexLock. The MutexKey body can mutate every field of the record atomically. This is the canonical fix when joint atomicity is required (e.g. account-to-account transfer).
defines record Balances source as Integer: 0 target as Integer: 0 default operator ?
defines component TransferService accountLock as MutexLock of Balances: MutexLock(Balances())
transfer()
-> amount as Integer
key <- (amount) is MutexKey of Balances as function
lockedItem.source: lockedItem.source - amount
lockedItem.target: lockedItem.target + amount
require accountLock.enter(key)
2. SEQUENTIAL — NEVER NESTED
If the two concerns don't need joint atomicity, acquire locks one at a time. Release the first before taking the second. Same code path, two enters, no nesting.
require lockA.enter(keyA) //completes and releases require lockB.enter(keyB) //independent operation
3. PICK A SINGLE OWNER FOR EACH CONCERN
If two methods on the same component want different locks, give each method exactly one lock. Different concerns → different methods → different locks. No method ever holds both.
WHY EK9 DETECTS THIS
Lock-order cycle deadlocks are notoriously hard to reproduce — they only manifest under specific thread interleavings. Catching them at compile time eliminates a whole class of production bugs. The precedence-graph approach is the same model the Linux kernel uses at runtime (lockdep); EK9 brings it forward to compile time thanks to the closed-world type system.
See Q158 for MutexLock basics (which shows the canonical LockableAddressSet pattern). See Q1299 for the cross-thread variant (E08253). See Q1300 for multi-lock design patterns.
Example
defines module qa.concurrency.deadlock.cycle defines record Balances source as Integer: 100 target as Integer: 0 default operator ? defines component //Canonical fix: ONE MutexLock guards a record containing both //resources. The MutexKey body mutates both fields atomically; //no second lock exists, so no cycle can form. TransferService accountLock as MutexLock of Balances: MutexLock(Balances()) transfer() -> amount as Integer transferKey <- (amount) is MutexKey of Balances as function //Both fields mutated atomically under one lock. lockedItem.source: lockedItem.source - amount lockedItem.target: lockedItem.target + amount require accountLock.enter(transferKey) default operator ? defines program LockOrderCycleFix() stdout <- Stdout() service <- TransferService() service.transfer(25) stdout.println("transfer complete — no deadlock possible")
Other ways to ask this
- How do I fix E08252 deadlock cycle?
- What is a lock-order cycle in EK9?
- EK9 says my locks form a cycle — what now?
- Two MutexLocks deadlock at compile time — how to restructure?
- How to fix nested enter different lock identity?
- Lock acquisition order error E08252
Coming from another language?
Java: deadlocks discovered at runtime via thread dumps and stress testing. Tools like FindBugs/SpotBugs flag some cases but can't see polymorphic dispatch or call chains. Go: deadlock detector in the runtime fires only when ALL goroutines block — partial deadlocks slip through. Rust: lock-order checked by convention; std library has no compile-time detection. EK9: workspace-wide static cycle detection on the lock precedence graph; refuses to compile if a cycle exists across any combination of call paths.
Keywords: deadlock, lock, enter, order, fix, transfer, nested, E08252, mutex, MutexLock, concurrent, atomic, race, cycle, restructure