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
- Only read `STM.TransactionId` inside the `sync`/`atomic` delegate.
- If the id is needed later, capture it into a local variable while the transaction is still running.
- 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
- Read STM.TransactionId only inside the sync/atomic delegate.
- Pass transaction metadata explicitly to helpers rather than reading ambient state.
- Capture the id early if it is needed after commit.
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
- Refs can only be written to from within a `sync` transaction
- Refs can only commute from within a transaction
- diff broken
- diff broken
- List is empty
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)