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