karatelabs/karate · error · JsErrorException

setTimeout: no engine is executing on this thread

Error message

setTimeout: no engine is executing on this thread

What it means

setTimeout validates that the calling context belongs to an Engine that is currently executing on the current thread (context.getEngine() != null and identical to Engine.current()). Scheduling a timer without a definite engine would leave the callback orphaned, so the library throws a TypeError rather than guessing.

Solutions

  1. Call setTimeout only from code running inside the engine's own evaluation on the same thread.
  2. Pass the Context from the active evaluation rather than a stale/captured one.
  3. Move scheduling into JS code evaluated by the engine instead of calling the host binding directly.

Example fix

// before: stale context on another thread
otherThreadContext.setTimeout(() -> ...);
// after: schedule inside engine evaluation
engine.evalRaw("setTimeout(() => done(), 100)");
Defensive patterns

Strategy: validation

Validate before calling

Engine current = Engine.current(); if (ctx == null || ctx.getEngine() == null || ctx.getEngine() != current) throw new IllegalStateException("setTimeout must run on the engine's own thread");

Try / catch

try { engine.evalRaw("setTimeout(cb, 100)"); } catch (JsErrorException e) { if (e.getMessage().startsWith("setTimeout:")) { /* fallback to Java scheduler */ } else throw e; }

Prevention

When it happens

Trigger: Calling setTimeout with a null Context, a context whose engine is null, or a context bound to an engine different from the one running on the current thread (e.g. setTimeout captured and called from another thread, or after the engine finished).

Common situations: Reusing a Context across threads in parallel step execution; calling setTimeout from a host callback that fires after evaluation ended; sharing contexts between two Engine instances.

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/151e6ac0911a921c. Report an issue: GitHub.

Appendix: source

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

    private static TimerScheduler scheduler;

    private static synchronized TimerScheduler scheduler() {
        if (scheduler == null || scheduler.isClosed() || scheduler.executor.isShutdown()) {
            scheduler = new TimerScheduler(Executors.newSingleThreadScheduledExecutor(
                    ThreadUtils.daemonFactory("js-timer-", false)));
            KarateLifecycle.register(scheduler);
        }
        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

View on GitHub (pinned to a22eb90246)