karatelabs/karate · error · JsErrorException

Maximum call stack size exceeded

Error message

Maximum call stack size exceeded

What it means

During synchronous function execution, a Java StackOverflowError escaping executeBody is translated into this JS RangeError so scripts get a normal, catchable JS error instead of crashing the host. It fires when JS recursion (direct or mutual) exceeds the thread stack.

Solutions

  1. Add or fix the recursion base case so it terminates.
  2. Convert recursion to iteration (explicit stack/loop) for deep data.
  3. Catch the RangeError at the boundary and fail gracefully with a diagnostic.
  4. Increase the host thread stack size (-Xss) only as a last resort.

Example fix

// before
function sum(arr, i) { return arr[i] + sum(arr, i + 1); } // no base case
// after
function sum(arr, i) { return i >= arr.length ? 0 : arr[i] + sum(arr, i + 1); }
Defensive patterns

Strategy: try-catch

Validate before calling

let depth = 0; function checkDepth() { if (++depth > 5000) throw new Error('recursion too deep'); }

Try / catch

try { return fn(); } catch (e) { if (e instanceof RangeError && /Maximum call stack/.test(e.message)) return { error: 'stack-overflow' }; throw e; }

Prevention

When it happens

Trigger: Unbounded recursion like function f(){ return f(); } f(), deep recursion on large nested data without a base case, mutually recursive functions with no termination, extremely deep data structures walked recursively.

Common situations: Recursive JSON/tree walkers over deeply nested API payloads, recursive template or formula evaluators, accidental infinite recursion after a refactor removed a base case.

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


AI-assisted analysis of karatelabs/karate@a22eb90246 (2026-09-12). Data as JSON: /api/errors/ae6e3c5fde462a88. Report an issue: GitHub.

Appendix: source

Thrown at karate-js/src/main/java/io/karatelabs/js/JsFunctionNode.java:302

    Object bindArgsAndExecute(CoreContext functionContext, CoreContext parentContext, Object[] args) {
        functionContext.privateEnv = privateEnv;
        if (generator) {
            // A generator call runs no body code — it returns the generator
            // object; the body executes on the generator's vthread one driver
            // step at a time. Parameter binding is deferred to the first
            // next(), the same documented deviation async has.
            Engine engine = functionContext.getEngine();
            return new JsGenerator(engine, this, functionContext, args);
        }
        if (async) {
            // Argument binding is part of the activation's startup, so it runs on
            // the activation thread under the startup-outcome protocol — not here.
            return AsyncSupport.callAsync(this, functionContext, args);
        }
        try {
            return executeBody(functionContext, parentContext, args);
        } catch (StackOverflowError e) {
            throw JsErrorException.rangeError("Maximum call stack size exceeded");
        }
    }

    /** The synchronous body run. For an async function this is what the
     *  activation thread executes; the caller has already been handed a promise. */
    Object executeBody(CoreContext functionContext, CoreContext parentContext, Object[] args) {
        // Attach the slot frame here — the single choke point every call path
        // shares before params bind and defaults evaluate. For an async function
        // this runs on the activation thread, so the frame exists before the
        // body's first statement there too; the frame lives on the context and
        // follows it across await suspensions.
        SlotTable table = slotTable;
        if (table == null && SlotTable.ENABLED && ++callCount == 2) {
            table = SlotTable.forNodeForced(node, argNodes, body);
            slotTable = table;
        }
        if (table != null) {
            functionContext.frame = table.newFrame();

View on GitHub (pinned to a22eb90246)