louthy/language-ext · error · NotSupportedException

You can't map a tail call

Error message

You can't map a tail call

What it means

IOTail<A> is an internal marker node in the LanguageExt IO DSL representing a tail-recursive call produced by the IO monad's trampoline. It is not a runnable effect, so Map cannot legally wrap it — mapping over a tail call would break the tail-call resolution that the trampoline performs in `resolve`. The library throws NotSupportedException deliberately to surface misuse of the DSL internals early instead of silently corrupting the recursion.

Solutions

  1. Do not call Map directly on the IOTail node; resolve the tail call first (via the trampoline / IO.run) and map on the resolved IO.
  2. Pattern-match: if you have IOTail<A> t and need IO<B>, use t.Tail (the wrapped IO<A>) and Map over that, ensuring typeof(A)==typeof(B) constraints of resolve are respected.
  3. Restructure your recursive IO so the transformation is applied inside the recursive step (e.g. map inside the Bind continuation) rather than over the tail-call boundary.
  4. If you are inspecting the AST, skip or unwrap IOTail nodes explicitly before applying combinators.

Example fix

// before
IO<int> mapped = tailResult.Map(x => x * 2); // throws
// after
IO<int> resolved = tailResult switch
{
    IOTail<int> t => t.Tail,          // unwrap the tail call
    var io => io
};
IO<int> mapped = resolved.Map(x => x * 2);
Defensive patterns

Strategy: type-guard

Validate before calling

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

Type guard

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

Try / catch

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

Prevention

When it happens

Trigger: Calling `.Map(f)` on an IO value that is actually an IOTail<A> — typically only reachable if you pattern-match into IO internals (DSL namespace) or call Map on the result of a tail-recursive `go` function before the trampoline resolves it.

Common situations: Developers writing custom IO interpreters or traversing the IO AST in LanguageExt.DSL; upgrading LanguageExt versions where IOTail became part of the public-ish DSL surface and code that previously saw IO monads now sees IOTail nodes.

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/49117818ab933d33. Report an issue: GitHub.

Appendix: source

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

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)