{"record":{"id":"4f19bc80544a16a6","repo":"louthy/language-ext","slug":"you-can-t-chain-a-tail-call","errorCode":null,"errorMessage":"You can't chain a tail call","messagePattern":"You can't chain a tail call","errorType":"exception","errorClass":"NotSupportedException","httpStatus":null,"severity":"error","filePath":"LanguageExt.Core/Effects/IO/DSL/IOTail.cs","lineNumber":13,"sourceCode":"using System;\nusing System.Threading.Tasks;\nusing LanguageExt.Traits;\n\nnamespace LanguageExt.DSL;\n\nrecord IOTail<A>(IO<A> Tail) : IO<A>\n{\n    public override IO<B> Map<B>(Func<A, B> f) => \n        throw new NotSupportedException(\"You can't map a tail call\");\n\n    public override IO<B> Bind<B>(Func<A, K<IO, B>> f) =>\n        throw new NotSupportedException(\"You can't chain a tail call\");\n\n    public override IO<B> BindAsync<B>(Func<A, ValueTask<K<IO, B>>> f) => \n        throw new NotSupportedException(\"You can't chain a tail call\");\n\n    public override string ToString() => \n        \"IO tail\";\n\n    public static IO<C> resolve<B, C>(A initialValue, IO<B> bindResult, Func<A, B, C> project)\n        => bindResult switch\n           {\n               IOTail<B> tail when typeof(B) == typeof(C) => (IO<C>)(object)tail.Tail,\n               IOTail<B> => throw new NotSupportedException(\"Tail calls can't transform in the `select`\"),\n               var mb => mb.Map(y => project(initialValue, y))\n           };\n}\n","sourceCodeStart":1,"sourceCodeEnd":29,"githubUrl":"https://github.com/louthy/language-ext/blob/2f0e3628242889774d4141960a35671a0280051f/LanguageExt.Core/Effects/IO/DSL/IOTail.cs#L1-L29","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nIO<B> chained = tailResult.Bind(next); // throws\n// after\nIO<B> chained = tailResult switch\n{\n    IOTail<A> t => t.Tail.Bind(next),   // bind on the unwrapped tail\n    var io => io.Bind(next)\n};","handlingStrategy":"type-guard","validationCode":"bool isBindable<A>(IO<A> io) => io is not IOTail<A>;","typeGuard":"IO<B> safeBind<A,B>(IO<A> io, Func<A, K<IO,B>> f) =>\n    io is IOTail<A> t ? t.Tail.Bind(f) : io.Bind(f);","tryCatchPattern":"try { chained = io.Bind(f); }\ncatch (NotSupportedException ex) when (ex.Message.Contains(\"tail call\"))\n{\n    chained = ((IOTail<A>)io).Tail.Bind(f);\n}","preventionTips":["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."],"tags":["languageext","io-monad","tail-call","bind","not-supported"],"backgroundTag":"unsupported-operation","analyzedSha":"2f0e3628242889774d4141960a35671a0280051f","analyzedAt":"2026-09-15T03:31:55.716Z","contentChangedAt":"2026-09-15T03:31:55.716Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}