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
- Move the Bind inside the recursion: continue chaining within the recursive step so the trampoline owns all continuations.
- Unwrap the tail: extract `tail.Tail` and Bind on that underlying IO if you know the recursion is finished from your perspective.
- Use the intended public APIs (Repeat, RepeatWhile, etc.) rather than hand-assembling chains over DSL nodes.
- 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
- Chain continuations inside the recursive step so the trampoline owns them.
- Use public combinators (Repeat, RepeatWhile, etc.) instead of assembling DSL node chains.
- Type-check for IOTail in any generic code that Binds over arbitrary IO<A> values.
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
- You can't map a tail call
- Tail calls can't transform in the `select`
- Invalid iterator
- Ord attribute should have a struct type that derives from…
- Hashable attribute should have a struct type that derives…
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)