louthy/language-ext · error · InvalidOperationException

Transaction not running

Error message

Transaction not running

What it means

`STM.TransactionId` is a static property returning the id of the currently running STM transaction. When no transaction is running on the current context, `transaction.Value` is null and the property throws `InvalidOperationException("Transaction not running")` instead of returning a sentinel id. It is a contract that you only query transaction metadata from within `sync`/`atomic`.

Solutions

  1. Only read `STM.TransactionId` inside the `sync`/`atomic` delegate.
  2. If the id is needed later, capture it into a local variable while the transaction is still running.
  3. If you need a nullable check, avoid the property and design your API to pass the transaction state explicitly.

Example fix

// before
var id = STM.TransactionId; // throws if no transaction

// after
STM.sync(() =>
{
    var id = STM.TransactionId; // safe
    return unit;
});
Defensive patterns

Strategy: validation

Validate before calling

// capture the id inside the transaction instead of reading it later
long id = STM.sync(() => { var i = STM.TransactionId; /* ... */ return i; });

Try / catch

long? id = null;
try { id = STM.TransactionId; }
catch (InvalidOperationException) { /* no transaction running */ }

Prevention

When it happens

Trigger: Reading `STM.TransactionId` from code outside any `STM.sync`/`atomic` block — e.g. logging helpers called after the transaction finished, or code executed on a different async context than the transaction.

Common situations: Diagnostics/logging inside helper methods that assume a transaction is active; async callbacks scheduled during the transaction that fire after it completes; custom Ref-like types querying the transaction id outside 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/2373da1b4d84ff7c. Report an issue: GitHub.

Appendix: source

Thrown at LanguageExt.Core/Concurrency/STM/STM.cs:505

    /// <summary>
    /// Conflict exception for internal use
    /// </summary>
    class ConflictException : Exception;

    /// <summary>
    /// Wraps a (A -> A) predicate as (object -> object)
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    static Func<object, object> CastCommute<A>(Func<A, A> f) =>
        obj => f((A)obj)!;

    /// <summary>
    /// Get the currently running TransactionId
    /// </summary>
    public static long TransactionId
    {
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        get => transaction.Value?.transactionId ?? throw new InvalidOperationException("Transaction not running");
    }

    /// <summary>
    /// Transaction snapshot
    /// </summary>
    class Transaction
    {
        static long transactionIdNext;
        public readonly long transactionId;
        public TrieMap<EqLong, long, RefState> state;
        public TrieMap<EqLong, long, Change<RefState>> changes;
        public readonly System.Collections.Generic.HashSet<long> reads = new();
        public readonly System.Collections.Generic.HashSet<long> writes = new();
        public readonly System.Collections.Generic.List<(long Id, Func<object, object> Fun)> commutes = new();

        public static readonly Transaction None = new (TrieMap<EqLong, long, RefState>.Empty);

        [MethodImpl(MethodImplOptions.AggressiveInlining)]

View on GitHub (pinned to 2f0e362824)