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
- Call setTimeout only from code running inside the engine's own evaluation on the same thread.
- Pass the Context from the active evaluation rather than a stale/captured one.
- 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
- Never share a Context across threads.
- Schedule timers from JS code evaluated by the engine itself.
- Verify Engine.current() matches the context's engine before host-side timer calls.
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.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- setTimeout requires an active evaluation
- setTimeout: karate is shutting down
- setTimeout:
- engine is poisoned:
- async function called outside an engine evaluation
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 beView on GitHub (pinned to a22eb90246)