karatelabs/karate · error · EngineException

await on a promise that can never settle

Error message

await on a promise that can never settle

What it means

While pumping jobs waiting for a promise, if the scope becomes quiescent (no pending jobs, nothing to run) but the promise is still unsettled, AsyncSupport throws EngineException 'await on a promise that can never settle' — no remaining work could ever resolve it, so waiting longer is pointless.

Solutions

  1. Ensure every promise has a code path that calls resolve/reject regardless of control flow
  2. Verify you await the correct promise instance and that its producer was actually scheduled before awaiting
  3. Reproduce with logging in the promise executor to confirm whether the executor body ever runs
  4. Catch EngineException, inspect the promise creation site, and restructure so settlement is owned by scheduled scope jobs

Example fix

// before (JS)
let p = new Promise(() => {});      // executor never settles
await p;
// after (JS)
let p = new Promise((resolve) => setTimeout(() => resolve(42), 10));
await p; // always settles
Defensive patterns

Strategy: try-catch

Validate before calling

// JS: wrap promise creation so accidental never-settling executors throw in dev
let p = new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('producer did not settle')), 30_000); // watchdog
});

Try / catch

try {
    Object result = engine.await(promise);
} catch (EngineException e) {
    if (e.getMessage() != null && e.getMessage().contains("never settle")) {
        throw new ScriptBugException("promise producer never settles — fix the producer, not the await");
    }
    throw e;
}

Prevention

When it happens

Trigger: Calling await(promise) where the promise was created but its resolving job already ran without settling it, resolution depends on a callback that was never registered, or the promise's producer was dropped during scope teardown.

Common situations: Promise constructed with a resolve captured in a closure that is never invoked; awaiting a promise after the engine/scope has finished its jobs (e.g. post-teardown code); races where the settle happens on a thread not owned by the scope.

Related errors


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

Appendix: source

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

        // On an abrupt exit the lock is deliberately NOT reacquired: a running
        // activation may hold it, and this thread is on its way to tearing the
        // scope down. Every enclosing frame unlocks conditionally for that reason.
        while (!promise.isSettled()) {
            if (Thread.currentThread().isInterrupted() || scope.isCancelRequested()) {
                throw new EngineInterruptedException();
            }
            if (deadlineNanos != 0 && System.nanoTime() > deadlineNanos) {
                throw new EngineTimeoutException("async drain timed out");
            }
            AsyncJob job = scope.takeJob(POLL_MILLIS);
            if (job instanceof AsyncJob.Control control) {
                job.token.release();
                throw new EngineInterruptedException(control.reason);
            }
            if (job != null) {
                runJob(engine, job);
            } else if (scope.isQuiescent() && !promise.isSettled()) {
                throw new EngineException("await on a promise that can never settle", null);
            }
        }
        engine.reacquireJsLock(holds);
        return settledResult(promise);
    }

    //=== adoption =====================================================================================================

    /**
     * The one adoption operation, shared by the {@code Promise} constructor's
     * resolve, {@code Promise.resolve}, async-function return, {@code .then}
     * results and {@code CompletionStage} wrapping. JS-level rules run before
     * any CF composition.
     */
    static void resolveValue(JsPromise target, Object value, AsyncToken retiring) {
        if (value == target) {
            target.settle(true, JsErrorException.typeError("Chaining cycle detected for promise").payload, retiring);
            return;

View on GitHub (pinned to a22eb90246)