louthy/language-ext · info · OperationCanceledException

OperationCanceledException

Error message

OperationCanceledException

What it means

Iterable.AsyncEnumerable.AsEnumerableIO converts an IO<IAsyncEnumerable<A>> into a synchronous IEnumerable<A> by blocking on ToBlockingEnumerable with the environment token. If that token is cancelled, both ToBlockingEnumerable and the explicit per-element check throw OperationCanceledException. The library surfaces cancellation this way so callers can abort a blocking enumeration deterministically.

Solutions

  1. Catch OperationCanceledException around the enumeration and treat it as graceful shutdown
  2. Increase or remove the timeout on the CancellationTokenSource feeding EnvIO.New
  3. Consume the source asynchronously with AsAsyncEnumerableIO() and await foreach instead of blocking
  4. Ensure the async producer completes or yields control so the blocking drain is not starved while the token stays alive

Example fix

// before
foreach (var x in iterable.AsEnumerableIO().Run(env)) { ... }
// after
try
{
    foreach (var x in iterable.AsEnumerableIO().Run(env)) { ... }
}
catch (OperationCanceledException) when (env.Token.IsCancellationRequested)
{
    // cancellation requested: exit cleanly
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (env.Token.IsCancellationRequested) return; // don't start a blocking drain on a cancelled token

Try / catch

try { foreach (var x in seq) { ... } }
catch (OperationCanceledException) when (token.IsCancellationRequested) { /* cancelled */ }

Prevention

When it happens

Trigger: Calling AsEnumerableIO() on an Iterable whose underlying source is an IAsyncEnumerable<A>, then enumerating the returned sequence after the EnvIO token was cancelled (cts.Cancel(), timeout expiry, linked token fired); the check fires before each yield return at line 28.

Common situations: Bridging async streams to sync code (unit tests, legacy callers) with a test timeout cancelling the token; host shutdown mid-drain; using CancellationTokenSource.CreateLinkedTokenSource with a deadline that elapses during a slow async producer.

Related errors


AI-assisted analysis of louthy/language-ext@2f0e362824 (2026-09-15). Data as JSON: /api/errors/539b6fb0d65497d3. Report an issue: GitHub.

Appendix: source

Thrown at LanguageExt.Core/Immutable Collections/Iterable/DSL/Iterable.AsyncEnumerable.cs:28

sealed class IterableAsyncEnumerable<A>(IO<IAsyncEnumerable<A>> runEnumerable) : Iterable<A>
{
    internal override bool IsAsync =>
        true;

    public override IO<int> CountIO() =>
        IO.liftVAsync(async env => await (await runEnumerable.RunAsync(env)).CountAsync(env.Token));

    public override IO<IEnumerable<A>> AsEnumerableIO()
    {
        return IO.lift(env => go(env, runEnumerable));

        static IEnumerable<A> go(EnvIO env, IO<IAsyncEnumerable<A>> run)
        {
            var xs = run.Run(env);
            foreach (var x in xs.ToBlockingEnumerable(env.Token))
            {
                if (env.Token.IsCancellationRequested) throw new OperationCanceledException();
                yield return x;
            }
        }
    }

    public override IO<IAsyncEnumerable<A>> AsAsyncEnumerableIO() 
    {
        return IO.lift(env => go(env, runEnumerable));

        static async IAsyncEnumerable<A> go(EnvIO env, IO<IAsyncEnumerable<A>> run)
        {
            var xs = await run.RunAsync(env);
            await foreach (var x in xs.WithCancellation(env.Token))
            {
                yield return x;
            }
        }
    }

View on GitHub (pinned to 2f0e362824)