louthy/language-ext · error · InvalidOperationException
Refs can only commute from within a transaction
Error message
Refs can only commute from within a transaction
What it means
`STM.Commute` registers a commutative operation on a Ref (one whose effect is independent of ordering). Like all ref operations, it requires an active STM transaction; `STM.Commute` checks the ambient transaction and throws `InvalidOperationException` when none is running. Commute entries are buffered and applied at commit time, so a transaction must exist.
Solutions
- Move the commute call inside a transaction: `STM.sync(() => { ref.Commute(f); return unit; });`
- If the update doesn't need ordering-independence, use a regular in-transaction write (`ref.Value = ...`) inside `sync` instead.
- Verify the call isn't escaping the sync delegate via a captured lambda that runs later (e.g. inside Task.Run within sync).
Example fix
// before
refValue.Commute(x => x + 1); // throws outside transaction
// after
STM.sync(() => { refValue.Commute(x => x + 1); return unit; }); Defensive patterns
Strategy: try-catch
Validate before calling
// Only call Commute from within the sync delegate; assert ambient transaction there. Debug.Assert(inSync, "Commute requires STM.sync");
Try / catch
try { STM.sync(() => { r.Commute(f); return unit; }); }
catch (InvalidOperationException ex) when (ex.Message.Contains("commute")) { /* handle */ } Prevention
- Group all commute/read/write calls together inside one sync block.
- Never schedule commute calls via Task.Run or timers outside the transaction.
- Document commute methods as transaction-only in your API surface.
When it happens
Trigger: Calling `ref.Commute(f)` (or `STM.Commute` indirectly via a ref's commute method) outside of an `STM.sync`/`atomic` block — e.g. from plain code, a timer callback, or after the sync delegate has already returned.
Common situations: Attempting to enqueue commutative updates from background tasks outside the transaction; forgetting that commute, like read/write, is transaction-only; porting Clojure `commute` code and omitting the enclosing `dosync`/`sync`.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Refs can only be written to from within a `sync` transaction
- Transaction not running
- diff broken
- diff broken
- List is empty
AI-assisted analysis of louthy/language-ext@2f0e362824 (2026-09-15).
Data as JSON: /api/errors/2d0d688ebad09800.
Report an issue: GitHub.
Appendix: source
Thrown at LanguageExt.Core/Concurrency/STM/STM.cs:431
///
/// and returns the in-transaction-value when complete.
///
/// At the commit point of the transaction, `f` is run *AGAIN* with the
/// most recently committed value:
///
/// `f(most-recently-committed-value-of-ref)`
///
/// Thus `f` should be commutative, or, failing that, you must accept
/// last-one-in-wins behavior.
///
/// Commute allows for more concurrency than just setting the items
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static A Commute<A>(long id, Func<A, A> f)
{
if (transaction.Value == null)
{
throw new InvalidOperationException("Refs can only commute from within a transaction");
}
return (A)transaction.Value.Commute(id, CastCommute(f));
}
/// <summary>
/// Must be called in a transaction. Sets the in-transaction-value of
/// ref to:
///
/// `f(in-transaction-value-of-ref)`
///
/// and returns the in-transaction-value when complete.
///
/// At the commit point of the transaction, `f` is run *AGAIN* with the
/// most recently committed value:
///
/// `f(most-recently-committed-value-of-ref)`
///
/// Thus `f` should be commutative, or, failing that, you must acceptView on GitHub (pinned to 2f0e362824)