louthy/language-ext · error · BottomException

BottomException

Error message

BottomException

What it means

unsafeRecur implements a trampoline for tail-recursive monadic computation: the bound function must return a Next<A,B> that is either Loop (continue) or Done (finish). If the Next value is neither (an invalid/default Next state), the library cannot proceed and throws BottomException, LanguageExt's signal for a non-terminable / undefined computation state.

Solutions

  1. Return Next.Loop(v, f) or Next.Done(v) exclusively from the recursive function — never a default(Next<A,B>) or partially initialized value.
  2. Use the library's Recur/TailRec helper APIs instead of constructing Next values manually.
  3. Inspect the value passed in the failing iteration; a non-deterministic branch may be skipping both constructors.
  4. Verify LanguageExt package versions are consistent across the solution.

Example fix

// before
Func<int, K<M, Next<int, int>>> f = n =>
    n > 0 ? default(Next<int, int>) : Next<int, int>.Done(0);
// after
Func<int, K<M, Next<int, int>>> f = n =>
    n > 0 ? Next<int, int>.Loop(n - 1) : Next<int, int>.Done(0);
Defensive patterns

Strategy: validation

Validate before calling

// Ensure the recursive function only ever returns Loop or Done
var next = f(value);
if (!next.IsLoop && !next.IsDone) throw new ArgumentException("f must return Next.Loop or Next.Done");

Type guard

bool IsValidNext<A, B>(Next<A, B> n) => n.IsLoop || n.IsDone;

Try / catch

try { return Monad<M>.unsafeRecur(value, f); }
catch (BottomException ex) { throw new InvalidOperationException("Trampoline function returned an invalid Next state (neither Loop nor Done)", ex); }

Prevention

When it happens

Trigger: Calling Monad.Module.unsafeRecur (directly or via the Recur/TailRec helpers) with a function f that returns a Next<A,B> constructed as a default/invalid instance, or one whose discriminators IsLoop/IsDone are both false — typically from misuse of the Next type's constructors or custom Next implementations.

Common situations: Writing custom trampolined recursion and manually constructing Next values instead of using the provided Loop/Done constructors; switching on Next incorrectly and falling through to a bare default; library-version mismatches where Next's shape changed.

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/6c8ac44f63d96e3e. Report an issue: GitHub.

Appendix: source

Thrown at LanguageExt.Core/Traits/Monads/Monad/Monad.Module.cs:215

    /// and the users of `Monad.recur` would rightly be miffed if an implementation yielded a
    /// stack-overflow, so use this function with caution.
    /// </summary>
    /// <param name="value">Initial value to start the recursive process</param>
    /// <param name="f">Bind function that returns a monad with the bound value wrapped by `Next`, which
    /// enables decision-making about whether to recur, or not.</param>
    /// <typeparam name="M">Monad type</typeparam>
    /// <typeparam name="A">Loop value</typeparam>
    /// <typeparam name="B">Done value</typeparam>
    /// <returns>Monad structure</returns>
    /// <exception cref="BottomException"></exception>
    [Pure]
    public static K<M, B> unsafeRecur<M, A, B>(A value, Func<A, K<M, Next<A, B>>> f)
        where M : Monad<M> =>
        f(value).Bind(n => n switch
                           {
                               { IsLoop: true, Loop: var v } => unsafeRecur(v, f),
                               { IsDone: true, Done: var v } => M.Pure(v),
                               _                                  => throw new BottomException()
                           });

    /// <summary>
    /// Allow for tail-recursion by using a trampoline function that returns a monad with the bound value
    /// wrapped by `Next`, which enables decision-making about whether to keep the computation going or not.  
    /// </summary>
    /// <remarks>
    /// This is a handy pre-built version of `Monad.Recur` that works with `Iterable` (a lazy stream that supports
    /// both synchronicity and asynchronicity).  The `Natural` and `CoNatural` constraints allow any type that can
    /// convert to and from `Iterable` to gain this prebuilt stack-protecting recursion.  
    /// </remarks>
    /// <param name="value">Initial value to start the recursive process</param>
    /// <param name="f">Bind function that returns a monad with the bound value wrapped by `Next`, which
    /// enables decision-making about whether to recur, or not.</param>
    /// <typeparam name="M">Monad type</typeparam>
    /// <typeparam name="A">Loop value</typeparam>
    /// <typeparam name="B">Done value</typeparam>
    /// <returns>Monad structure</returns>

View on GitHub (pinned to 2f0e362824)