karatelabs/karate · error · RuntimeException

karate.callSingle() requires a Suite context

Error message

karate.callSingle() requires a Suite context

What it means

executeCallSingle implements karate.callSingle(), which caches a called feature/JS result at Suite level (with optional disk persistence of JSON-like results). It needs both a FeatureRuntime and a Suite; when either is missing this RuntimeException is thrown. callSingle is inherently a suite-scope API.

Solutions

  1. Run features through Runner.path(...) / the JUnit runner so a Suite is created before callSingle is used.
  2. In embedded usage, build and pass a Suite to the FeatureRuntime you construct.
  3. Replace callSingle with feature-scoped karate.callonce() when only per-feature caching is needed.
  4. For pure JS helpers, evaluate them directly instead of routing through callSingle.

Example fix

// before: embedded engine, no suite
var engine = new ScenarioRuntime(fr, scenario); // fr has no Suite
var v = engine.evalJs("karate.callSingle('data.js')");

// after
Runner.path("classpath:my.feature").parallel(1); // Suite-backed execution
Defensive patterns

Strategy: validation

Validate before calling

// embedded-usage check before using callSingle
boolean suiteReady = featureRuntime != null && featureRuntime.getSuite() != null;
if (!suiteReady) {
    // fall back to plain JS or feature-scoped callonce
}

Try / catch

try {
    var data = karate.callSingle('classpath:data.js');
} catch (e) {
    if (String(e).indexOf('requires a Suite context') >= 0) {
        karate.log('no Suite; falling back to callonce');
        data = karate.callonce('classpath:data.js');
    } else { throw e; }
}

Prevention

When it happens

Trigger: Calling karate.callSingle() from a ScenarioRuntime whose FeatureRuntime has no Suite (e.g. a feature invoked programmatically outside a Suite run), or from JS evaluated with no feature runtime attached at all.

Common situations: Embedding the Karate engine in an application and invoking features without Runner's Suite; unit tests constructing ScenarioRuntime directly; parallel-runner setups where a custom harness drops the Suite reference.

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

Appendix: source

Thrown at karate-core/src/main/java/io/karatelabs/core/ScenarioRuntime.java:693

     *
     * Flow:
     * 1. Check in-memory cache (lock-free for fast path)
     * 2. If callSingleCache is configured, check disk cache
     * 3. If not cached, acquire Suite lock
     * 4. Double-check cache (another thread may have cached while waiting)
     * 5. Execute and cache result (in-memory and optionally to disk)
     * 6. Return deep copy to prevent cross-thread mutation
     *
     * Disk caching (configure callSingleCache):
     * - { minutes: 15 } - cache to disk for 15 minutes (default dir: karate-temp/cache)
     * - { minutes: 15, dir: 'some/folder' } - custom cache directory
     * - Only JSON-like results (Map/List) are persisted to disk
     *
     * Exceptions are cached and re-thrown on subsequent calls.
     */
    public Object executeCallSingle(String path, Object arg) {
        if (featureRuntime == null || featureRuntime.getSuite() == null) {
            throw new RuntimeException("karate.callSingle() requires a Suite context");
        }

        Suite suite = featureRuntime.getSuite();
        Map<String, Object> cache = suite.getCallSingleCache();
        ReentrantLock lock = suite.getCallSingleLock();

        // Get disk cache settings from config
        int cacheMinutes = config.getCallSingleCacheMinutes();
        String cacheDir = config.getCallSingleCacheDir();
        if (cacheDir == null || cacheDir.isEmpty()) {
            // Default to <buildDir>/karate-temp/cache (cleaned by 'karate clean')
            cacheDir = io.karatelabs.common.FileUtils.getBuildDir() + "/karate-temp/cache";
        }

        // Fast path: check if already in memory cache (no locking needed)
        if (cache.containsKey(path)) {
            logger.trace("[callSingle] memory cache hit: {}", path);
            return unwrapCachedResult(cache.get(path));

View on GitHub (pinned to a22eb90246)