karatelabs/karate · error · JsErrorException

setTimeout requires an active evaluation

Error message

setTimeout requires an active evaluation

What it means

setTimeout requires not only a live engine on the thread but an active AsyncScope evaluation. The scope is what tracks pending timers and drives the drain loop; with no scope in progress there is nothing to attach the timer to, so the call is rejected with a TypeError.

Solutions

  1. Schedule timers only from within an async evaluation (e.g. inside an async function invoked through the engine's async path).
  2. If you need delayed work in sync code, perform it on the Java side instead of via setTimeout.
  3. Ensure the engine's currentScope() is active — don't defer setTimeout calls past the end of the evaluation.

Example fix

// before: sync eval
evalRaw("setTimeout(cb, 50)"); // throws
// after: async evaluation
await engine.evalRaw("(async () => { await delay(50); cb(); })()");
Defensive patterns

Strategy: validation

Validate before calling

if (engine.currentScope() == null) throw new IllegalStateException("setTimeout needs an active async evaluation");

Try / catch

try { engine.evalRaw("setTimeout(cb, 50)"); } catch (JsErrorException e) { if (e.getMessage().equals("setTimeout requires an active evaluation")) { /* do the delayed work in Java */ } else throw e; }

Prevention

When it happens

Trigger: Calling setTimeout from engine code executing outside an async evaluation — e.g. a synchronous top-level eval where no AsyncScope was pushed, or after the async scope completed.

Common situations: Using setTimeout in a plain synchronous JS evaluation (no await/async root); calling it during host-driven evaluation that didn't open an async scope; timers scheduled after the async block already returned.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

Thrown at karate-js/src/main/java/io/karatelabs/js/AsyncSupport.java:738

        return scheduler;
    }

    /** Owner resolution is from the call's own context, validated against the
     *  thread's current engine — scheduling without a definite engine is an
     *  error, not a guess. */
    private static Engine timerEngine(Context context) {
        Engine engine = context == null ? null : context.getEngine();
        if (engine == null || engine != Engine.current()) {
            throw JsErrorException.typeError("setTimeout: no engine is executing on this thread");
        }
        return engine;
    }

    static Object setTimeout(Context context, Object[] args) {
        Engine engine = timerEngine(context);
        AsyncScope scope = engine.currentScope();
        if (scope == null) {
            throw JsErrorException.typeError("setTimeout requires an active evaluation");
        }
        Object callback = arg(args, 0);
        if (!(callback instanceof JsCallable target)) {
            throw JsErrorException.typeError("setTimeout: callback is not a function");
        }
        long delay = coerceDelay(arg(args, 1));
        Object[] extra = args.length > 2 ? Arrays.copyOfRange(args, 2, args.length) : EMPTY_ARGS;
        if (KarateLifecycle.phase() != KarateLifecycle.Phase.RUNNING) {
            // refuse before a token is acquired — scheduling is about to be
            // rejected anyway, and an unbacked token would strand the drain
            throw new EngineInterruptedException("setTimeout: karate is shutting down");
        }
        AsyncToken token = scope.acquire("timer");
        if (token == null) {
            throw new EngineInterruptedException();
        }
        AsyncActivation.anyAsync = true;
        int id = scope.nextTimerId();

View on GitHub (pinned to a22eb90246)