Bun, JavaScript, and TCO
Tail call optimization in JavaScript – particularly the “proper tail calls” mandated by the ECMAScript spec – is drawing renewed attention because Bun (via JavaScriptCore) supports it while major engines like V8 and SpiderMonkey largely do not. Commenters weigh the practical benefits for recursion-heavy or functional styles (e.g. avoiding stack overflows, CPS, state machines) against drawbacks such as harder debugging, altered stack semantics, and poor performance when combined with JS array operations and mutation. The exchange also touches on broader questions: whether JavaScript should lean further into functional paradigms, how much engines should follow the spec when it changes observable behavior, and the risk of code that works in Bun but fails or slows down in Node or Deno.
Meaning of “TCO” and expectations from title
- In this context, TCO = Tail Call Optimization (or “proper tail calls”), not Total Cost of Ownership or other acronyms.
- Several readers initially expected an article about Total Cost of Ownership for Bun/JS and were mildly disappointed but still found the functional angle interesting.
Recursion, TCO, and JavaScript semantics
- Tail-call optimization is framed as more than an “optimization”: when guaranteed, it changes language semantics by preventing stack growth for tail calls.
- Proper tail calls are mandated by the ECMAScript spec in strict mode, but most major engines (V8, SpiderMonkey) ignore this part. WebKit/JSC and Bun (which uses JSC) do implement them.
- Some argue lack of TCO limits expressiveness (e.g., CPS, mutually recursive functions, state machines encoded as tail-calling functions).
Reasons major JS engines avoid PTC
- Main cited issue: developer experience. TCO elides stack frames, making stack traces, debuggers, and error-reporting tools less faithful to actual call structure.
- There are mentions of security/realm-boundary complications and implementation complexity.
- Others counter that debuggers can maintain shadow stacks or metadata; Scheme and Lua show this is solvable.
- Some see the resistance as partly “political” or anti-functional, not purely technical.
Performance and code style debates
- Big distinction between:
- Tail-recursive but allocation-heavy code (e.g., building new arrays with spread each call), which is asymptotically bad even with TCO.
- Imperative loops or mutation-based versions, which are much faster and more memory-efficient in JS engines.
- Many participants find the recursive/ternary example hard to read compared to a simple loop; some call it “clever but illegible.”
- Others defend functional style as perfectly readable for those used to it, but concede JS’s arrays and GC make naive functional patterns inefficient.
Scope and practicality of TCO in JS
- Several argue TCO is essential in purely functional languages, but less critical in a multi-paradigm, mutable language where loops are idiomatic.
- Concern: code relying on Bun/JSC TCO may stack-overflow on Node/Deno; runtime checks or comments are advised.
- Python’s deliberate lack of TCO (for similar debugging/readability reasons) is referenced as a parallel.
Bun- and engine-specific notes
- Bun exposes a JSC intrinsic for tail calls via a special
$vminterface, but misuse can crash the process. - Bun is praised for speed (e.g., dependency installs); runtime parity with Node/Next.js app router is still incomplete but on the roadmap.
- For AWS Lambda, Bun can be used via custom runtimes/containers, but without Node-like cold-start optimizations.