louthy/language-ext · error · InvalidOperationException

Invalid iterator

Error message

Invalid iterator

What it means

IO.RepeatUntil builds its iteration via an internal enumerable/iterator of IO steps; a switch in the implementation expects a specific head shape (a Cons-like iterator node). If the head is anything else — an empty/terminated iterator or an unexpected IO node — the implementation cannot construct the loop and throws InvalidOperationException("Invalid iterator"). Repeat and RepeatWhile funnel through this code path.

Solutions

  1. Ensure the repeated effect is a non-empty, standard IO (e.g. built with IO.pure/IO.lift or plain effects), not an empty/custom iterator node.
  2. Construct the loop with the intended public API: `effect.RepeatUntil(predicate)` on a real effect rather than assembling DSL nodes manually.
  3. If a custom IO node is involved, make it derive from a recognized IO form, or perform your own recursive loop with Bind and a termination check.
  4. Check your LanguageExt version; if you rely on internal iterator shapes, align with the representation in your installed version.

Example fix

// before — feeds an empty/custom iterator node into the repeater
IO<int> loop = customIteratorNode.RepeatUntil(v => done(v)); // throws
// after — recursive Bind loop with explicit termination
IO<int> loop = default(IO<int>);
loop = IO<int>.lift(() => runStep())
      .Bind(v => predicate(v) ? IO.pure(v) : go(tail, v));
// or simply: bodyEffect.RepeatUntil(v => predicate(v)) on a standard IO
Defensive patterns

Strategy: validation

Validate before calling

static IO<T> requireNonEmpty<T>(IO<T> io, string ctx) =>
    io is null || io is IOTail<T>
        ? throw new ArgumentException($"{ctx}: effect must be a standard non-empty IO")
        : io;

Type guard

bool isRepeatable<T>(IO<T> io) =>
    io is not null && io is not IOTail<T> && io is not IO<T>.Empty;

Try / catch

try { loop = effect.RepeatUntil(p); }
catch (InvalidOperationException ex) when (ex.Message == "Invalid iterator")
{
    loop = runManualLoop(effect, p); // fallback recursive Bind loop
}

Prevention

When it happens

Trigger: Calling IO.RepeatUntil / Repeat / RepeatWhile with an effect whose internal iteration source yields no steps or a non-standard IO node — typically when the head IO passed to Repeat is not one of the recognized iterator forms the implementation switches on.

Common situations: Passing an empty or immediately-completed IO as the looping body; building the repeated effect through custom DSL nodes that the internal switch doesn't recognize; library version changes where iterator representation changed and old constructed nodes no longer match.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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

Appendix: source

Thrown at LanguageExt.Core/Effects/IO/IO.cs:545

        Schedule schedule,
        Func<A, bool> predicate)
    {
        return go(schedule.PrependZero.Run().GetIterator(), default);

        IO<A> go(Iterator<Duration> iter, A? value) =>
            iter switch
            {
                Iterator<Duration>.Nil =>
                    IO.pure<A>(value!),

                Iterator<Duration>.Cons(var head, var tail) =>
                    IO.yieldFor(head)
                      .Bind(_ => Bracket()
                               .Bind(v => predicate(v) 
                                              ? IO.pure(v) 
                                              : go(tail, v))),
                
                _ => throw new InvalidOperationException("Invalid iterator")
            };
    }
    
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    //
    //  Retrying the effect when it fails
    //

    /// <summary>
    /// Retry if the IO computation fails 
    /// </summary>
    /// <remarks>
    /// This variant will retry forever
    /// </remarks>
    /// <remarks>
    /// Any resources acquired within a retrying IO computation will automatically be released *if* the operation fails.
    /// So, successive retries will not grow the acquired resources on each retry iteration.  Any successful operation that
    /// acquires resources will have them tracked in the usual way. 

View on GitHub (pinned to 2f0e362824)