louthy/language-ext · error · NotSupportedException

You can't chain a tail call

Error message

You can't chain a tail call

What it means

Same rationale as Map: IOTail<A> marks a pending tail-recursive invocation inside the IO trampoline. Binding (chaining) over it is impossible because the tail call's continuation is owned by the trampoline's `resolve` machinery; adding another Bind would double-bind and break the recursion protocol, so the library refuses it.

Solutions

  1. Move the Bind inside the recursion: continue chaining within the recursive step so the trampoline owns all continuations.
  2. Unwrap the tail: extract `tail.Tail` and Bind on that underlying IO if you know the recursion is finished from your perspective.
  3. Use the intended public APIs (Repeat, RepeatWhile, etc.) rather than hand-assembling chains over DSL nodes.
  4. If writing an interpreter, treat IOTail specially in your fold rather than generically calling Bind on it.

Example fix

// before
IO<B> chained = tailResult.Bind(next); // throws
// after
IO<B> chained = tailResult switch
{
    IOTail<A> t => t.Tail.Bind(next),   // bind on the unwrapped tail
    var io => io.Bind(next)
};
Defensive patterns

Strategy: type-guard

Validate before calling

bool isBindable<A>(IO<A> io) => io is not IOTail<A>;

Type guard

IO<B> safeBind<A,B>(IO<A> io, Func<A, K<IO,B>> f) =>
    io is IOTail<A> t ? t.Tail.Bind(f) : io.Bind(f);

Try / catch

try { chained = io.Bind(f); }
catch (NotSupportedException ex) when (ex.Message.Contains("tail call"))
{
    chained = ((IOTail<A>)io).Tail.Bind(f);
}

Prevention

When it happens

Trigger: Calling `.Bind(f)` on a value that is an IOTail<A> — e.g. chaining a continuation directly onto the result of a tail-recursive IO helper (like the internal Repeat/forever loops) instead of onto the resolved effect.

Common situations: Custom interpreters or combinators written against LanguageExt.DSL; code that captures the intermediate result of a recursive IO loop and tries to continue chaining; refactors that moved a Bind outside the recursive `go` function.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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

Appendix: source

Thrown at LanguageExt.Core/Effects/IO/DSL/IOTail.cs:13

using System;
using System.Threading.Tasks;
using LanguageExt.Traits;

namespace LanguageExt.DSL;

record IOTail<A>(IO<A> Tail) : IO<A>
{
    public override IO<B> Map<B>(Func<A, B> f) => 
        throw new NotSupportedException("You can't map a tail call");

    public override IO<B> Bind<B>(Func<A, K<IO, B>> f) =>
        throw new NotSupportedException("You can't chain a tail call");

    public override IO<B> BindAsync<B>(Func<A, ValueTask<K<IO, B>>> f) => 
        throw new NotSupportedException("You can't chain a tail call");

    public override string ToString() => 
        "IO tail";

    public static IO<C> resolve<B, C>(A initialValue, IO<B> bindResult, Func<A, B, C> project)
        => bindResult switch
           {
               IOTail<B> tail when typeof(B) == typeof(C) => (IO<C>)(object)tail.Tail,
               IOTail<B> => throw new NotSupportedException("Tail calls can't transform in the `select`"),
               var mb => mb.Map(y => project(initialValue, y))
           };
}

View on GitHub (pinned to 2f0e362824)