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

  1. Move the commute call inside a transaction: `STM.sync(() => { ref.Commute(f); return unit; });`
  2. If the update doesn't need ordering-independence, use a regular in-transaction write (`ref.Value = ...`) inside `sync` instead.
  3. 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

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


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 accept

View on GitHub (pinned to 2f0e362824)