{"record":{"id":"cd24964ae418b96f","repo":"louthy/language-ext","slug":"refs-can-only-be-written-to-from-within-a-sync-transaction","errorCode":null,"errorMessage":"Refs can only be written to from within a `sync` transaction","messagePattern":"Refs can only be written to from within a `sync` transaction","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"LanguageExt.Core/Concurrency/STM/STM.cs","lineNumber":403,"sourceCode":"    /// If within a transaction then the in-transaction value is returned, otherwise it's\n    /// the current latest value\n    /// </summary>\n    [MethodImpl(MethodImplOptions.AggressiveInlining)]\n    internal static object Read(long id) =>\n        transaction.Value == null\n            ? state.Items[id].UntypedValue\n            : transaction.Value.ReadValue(id);\n\n    /// <summary>\n    /// Write the value for the reference ID provided\n    /// Must be run within a transaction\n    /// </summary>\n    [MethodImpl(MethodImplOptions.AggressiveInlining)]\n    internal static void Write(long id, object value)\n    {\n        if (transaction.Value == null)\n        {\n            throw new InvalidOperationException(\"Refs can only be written to from within a `sync` transaction\");\n        }\n        transaction.Value.WriteValue(id, value);\n    }\n\n    /// <summary>\n    /// Must be called in a transaction. Sets the in-transaction-value of\n    /// ref to:  \n    /// \n    ///     `f(in-transaction-value-of-ref)`\n    ///     \n    /// and returns the in-transaction-value when complete.\n    /// \n    /// At the commit point of the transaction, `f` is run *AGAIN* with the\n    /// most recently committed value:\n    /// \n    ///     `f(most-recently-committed-value-of-ref)`\n    /// \n    /// Thus `f` should be commutative, or, failing that, you must accept","sourceCodeStart":385,"sourceCodeEnd":421,"githubUrl":"https://github.com/louthy/language-ext/blob/2f0e3628242889774d4141960a35671a0280051f/LanguageExt.Core/Concurrency/STM/STM.cs#L385-L421","documentation":"LanguageExt's STM (software transactional memory) `Ref<T>` values can only be mutated inside a transaction started via `sync(...)` or `atomic(...)`. `STM.Write` is the internal write path for refs; it checks the ambient (async-local) transaction and throws if none is running. This mirrors Clojure's rule that refs are transactional and cannot be mutated outside a `dosync` block.","triggerScenarios":"Calling `ref.Value = x` (or any ref mutation API that routes to STM.Write) from code that is not running inside an `STM.sync`/`atomic` block — e.g. mutating a Ref from a background thread, event handler, or top-level code where no transaction is active.","commonSituations":"Developers treat Ref<T> like a plain mutable cell and set its value outside of any `sync` block; mutating refs in async continuations where the transaction context was lost; unit tests that construct a Ref and write to it directly without wrapping in a transaction.","solutions":["Wrap the ref mutation in a transaction: `var result = STM.sync(() => { ref.Value = x; return ...; });`","If you don't need transactional semantics, use a plain mutable holder (e.g. `Ref` replaced by a simple class field, `Atom<T>`, or `Option` in a field) instead of a STM Ref.","Check that the code path actually executes inside `sync` — async/await boundaries or thread switches can drop the transaction context; do all ref writes within the sync delegate."],"exampleFix":"// before\nvar r = Ref.create(0);\nr.Value = 42; // throws: no transaction\n\n// after\nvar r = Ref.create(0);\nSTM.sync(() => { r.Value = 42; return unit; });","handlingStrategy":"try-catch","validationCode":"var insideTx = STM.TransactionRunning; // or track your own flag set inside sync\nif (!insideTx) throw new InvalidOperationException(\"Ref write requires STM.sync\");","typeGuard":"bool CanWriteRef() => !IsOutsideTransaction(); // expose your own ambient-transaction check","tryCatchPattern":"try { STM.sync(() => { r.Value = x; return unit; }); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"sync\")) { /* fallback: use Atom or plain field */ }","preventionTips":["Always perform Ref mutations inside the sync delegate, never capture-and-defer them.","Prefer Atom<T> when transactional semantics are not needed.","Add a code-review rule forbidding ref writes outside STM.sync blocks."],"tags":["stm","concurrency","transaction","csharp"],"backgroundTag":"invalid-state-transition","analyzedSha":"2f0e3628242889774d4141960a35671a0280051f","analyzedAt":"2026-09-15T03:31:55.716Z","contentChangedAt":"2026-09-15T03:31:55.716Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}