karatelabs/karate · error · EngineTimeoutException

async drain timed out

Error message

async drain timed out

What it means

AsyncSupport.drain pumps the event loop of a JS scope until it is quiescent, bounded by a deadline; if the deadline (deadlineNanos) elapses while async jobs are still pending, it throws EngineTimeoutException 'async drain timed out'.

Solutions

  1. Increase the drain timeout/deadline configured for the async scope to cover the slowest expected job
  2. Inspect pending jobs for callbacks that are never invoked (missing resolve/reject on error paths)
  3. Add error handling inside async jobs so failures settle the scope instead of hanging it
  4. Log timing of each job to identify which job is exceeding the deadline

Example fix

// before
scope.drain(500); // ms deadline too tight for remote calls
// after
scope.drain(30_000); // generous deadline matching worst-case job latency
Defensive patterns

Strategy: try-catch

Validate before calling

// before draining, ensure async work is bounded
long started = System.nanoTime();
// drain(deadlineMillis) must exceed the slowest scheduled job's worst case

Try / catch

try {
    scope.drain(deadlineMillis);
} catch (EngineTimeoutException e) {
    logger.warn("async jobs exceeded {}ms, pending jobs: {}", deadlineMillis, scope.pendingJobCount());
    scope.cancel();
    throw e;
}

Prevention

When it happens

Trigger: Calling drain (directly or via hostAsyncCall/finishScope) on a scope whose async jobs never finish within the configured timeout — long-blocking jobs, jobs that reschedule themselves, or callbacks that never complete.

Common situations: JS that awaits network calls slower than the drain deadline; an async job that swallows its completion callback on error; deadlines configured too aggressively for CI machines under load.

Understand the failure class

Related errors


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

Appendix: source

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

        }
        reportUnhandledRejections(engine, scope);
        return result;
    }

    static void drain(Engine engine, AsyncScope scope) {
        long deadlineNanos = drainDeadline(engine);
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                throw new EngineInterruptedException();
            }
            if (scope.isCancelRequested()) {
                throw new EngineInterruptedException();
            }
            if (scope.isQuiescent()) {
                return;
            }
            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) {
                // teardown is the pump owner's job, never the requesting
                // activation's — it would be joining its own thread
                job.token.release();
                throw new EngineInterruptedException(control.reason);
            }
            if (job != null) {
                runJob(engine, job);
            }
        }
    }

    private static long drainDeadline(Engine engine) {
        Duration cap = engine.asyncDrainTimeout();
        return cap == null ? 0 : System.nanoTime() + cap.toNanos();
    }

View on GitHub (pinned to a22eb90246)