I got E08251 SHARED_STATE_MUTATION_OUTSIDE_LOCK — why can't my async worker write shared state?

← Concurrency · Ref: Q1350

E08251 fires when code mutates shared state that crosses a thread boundary without doing so inside a MutexLock.enter(...) body. The most common trigger is capturing a shared object into a `| async` worker and writing one of its fields there — the worker runs on a different thread, so the write races with anyone else touching that object.

WHAT THE ERROR MEANS

A `| async` stage dispatches each element to a thread-pool worker. If the worker captures a shared record and writes its field, that write happens off the caller's thread with no synchronisation. EK9 requires every mutation of shared state to happen inside a MutexKey body guarded by a MutexLock.

HOW TO FIX

Either keep the mutation on the same thread (read-only in the worker, aggregate afterwards), or route the mutation through a MutexLock so the write is serialised. Reading shared state in a worker is fine — only mutation is rejected.

  //worker only READS; the write happens under a lock elsewhere
  worker <- (shared) is asyncWorkerFn as function
    rtn: shared.total > 0

WHY EK9 DETECTS THIS

Data races on shared mutable state are among the hardest bugs to reproduce. EK9 makes the thread boundary visible (`| async`) and tracks which state the worker captures, refusing any unsynchronised mutation.

Example

defines module qa.concurrency.shared.state.outside.lock

  defines record
    Tally
      total <- 0
      default operator ?

  defines function

    asyncWorkerFn() as abstract
      <- rtn as Boolean?

    toLine() as pure
      -> ok as Boolean
      <- rtn as String: $ok

    runPipeline()
      stdout <- Stdout()
      shared <- Tally()
      worker <- (shared) is asyncWorkerFn as function
        rtn: shared.total > 0
      workers <- [ worker ]
      cat workers | async | map with toLine > stdout

Common mistakes

E08251 — The async worker captures the shared `Tally` and writes `shared.total` on a thread-pool thread with no lock, racing any other access. The safe form only reads in the worker; any mutation must go through a MutexLock body. See ek9 -h E08251.

Incorrect:

shared.total: shared.total + 1
        rtn: true

Correct:

rtn: shared.total > 0
Other ways to ask this
  • Why must I write shared state inside a lock in EK9?
  • async worker mutates shared field error E08251
  • How to mutate shared data safely across an async boundary
  • captured write outside MutexLock EK9

Coming from another language?

Java/Go: nothing stops a worker/goroutine from mutating captured shared state; races are found (if ever) via stress testing or the race detector at runtime. EK9: mutation of thread-crossing shared state outside a MutexLock is a compile-time error.

Keywords: worker, state, concurrent, captured, MutexLock, mutation, lock, race, async, E08251, shared