louthy/language-ext · warning · OperationCanceledException

OperationCanceledException

Error message

OperationCanceledException

What it means

AsEnumerable on IteratorAsync checks the caller-supplied CancellationToken at each step and throws OperationCanceledException when cancellation was requested. Cooperatively cancels the enumeration loop.

Solutions

  1. Pass a non-cancelled token when you need full enumeration; only pass request/timeout tokens when aborting early is intended.
  2. Catch OperationCanceledException around the enumeration and treat it as normal cancellation (return 499/Cancelled, not 500).
  3. Use `token.ThrowIfCancellationRequested()`-aware patterns and cts.Token registration for cleanup before rethrowing.

Example fix

// before
await foreach (var x in it.AsEnumerable(ct)) Process(x);
// after
try { await foreach (var x in it.AsEnumerable(ct)) Process(x); }
catch (OperationCanceledException) when (ct.IsCancellationRequested) { /* expected cancel */ }
Defensive patterns

Strategy: try-catch

Validate before calling

if (ct.IsCancellationRequested) return; // check before starting enumeration
await foreach (var x in it.AsEnumerable(ct)) { ... }

Type guard

static bool IsUsableToken(CancellationToken ct) => ct.CanBeCanceled && !ct.IsCancellationRequested;

Try / catch

try { await foreach (var x in it.AsEnumerable(ct)) Process(x); }
catch (OperationCanceledException) when (ct.IsCancellationRequested) { /* cancellation is expected; clean up */ }

Prevention

When it happens

Trigger: Enumerating an IteratorAsync via AsEnumerable(token) (directly or through GetAsyncEnumerator) while/after the CancellationToken is cancelled — e.g. request abort in ASP.NET Core or a timeout token firing mid-iteration.

Common situations: HTTP request cancelled by the client while streaming results; Task timeout token expiring during a long async enumeration; sharing a linked CTS and one branch cancelling.

Related errors


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

Appendix: source

Thrown at LanguageExt.Core/Immutable Collections/IteratorAsync/IteratorAsync.cs:100

    /// this are to break the linked-list chain so that there isn't a big linked-list of objects in memory that
    /// can't be garbage collected. 
    /// </summary>
    /// <remarks>
    /// Any other iterator references that came before this one will terminate at this point.  Splitting the
    /// previous and subsequent iterators here. 
    /// </remarks>
    /// <returns>New iterator that starts from the current iterator position.</returns>
    public abstract IteratorAsync<A> Split();

    /// <summary>
    /// Create an `IEnumerable` from an `Iterator`
    /// </summary>
    [Pure]
    public async IAsyncEnumerable<A> AsEnumerable([EnumeratorCancellation] CancellationToken token)
    {
        for (var ma = Clone(); !await ma.IsEmpty; ma = await ma.Tail)
        {
            if (token.IsCancellationRequested) throw new OperationCanceledException();
            yield return await ma.Head;
        }
    }

    /// <summary>
    /// Functor map
    /// </summary>
    [Pure]
    public IteratorAsync<B> Select<B>(Func<A, B> f) =>
        Map(f);

    /// <summary>
    /// Functor map
    /// </summary>
    [Pure]
    public IteratorAsync<B> Map<B>(Func<A, B> f)
    {
        return Go(this, f).GetIteratorAsync();

View on GitHub (pinned to 2f0e362824)