karatelabs/karate · error · RuntimeException

process already started

Error message

process already started

What it means

ProcessHandle.start() launches the configured subprocess and is documented as callable only once. It uses an AtomicBoolean compareAndSet to enforce this; a second call finds `started` already true and throws 'process already started'. This is a deliberate state-machine guard, not an OS failure.

Solutions

  1. Call start()/fork() only once per ProcessHandle; store the returned handle and reuse it
  2. If you need a restart, create a new ProcessHandle with the same config instead of re-starting
  3. If start may race, keep a single owner that starts the process and hand the existing handle to other consumers

Example fix

// before
handle.fork();
handle.start(); // throws: already started
// after
ProcessHandle handle = Karate.fork(config); // start once
// reuse `handle` for listeners, waitForOutput, etc.
Defensive patterns

Strategy: type-guard

Type guard

boolean canStart(ProcessHandle h) {
    try {
        java.lang.reflect.Field f = ProcessHandle.class.getDeclaredField("started");
        f.setAccessible(true);
        return !((java.util.concurrent.atomic.AtomicBoolean) f.get(h)).get();
    } catch (Exception e) { return false; }
}

Try / catch

try {
    handle.start();
} catch (RuntimeException e) {
    if (!e.getMessage().equals("process already started")) throw e;
    // already running - reuse existing handle
}

Prevention

When it happens

Trigger: Calling start() twice on the same ProcessHandle, or calling a convenience path (fork()/jsGet start) after start() already ran; callers listed include fork, start, jsGet, testOnStdOutChaining, and testDeferredStart.

Common situations: Accidentally calling both fork() and start(), re-invoking start() in retry logic, or sharing one ProcessHandle across threads/code paths that each attempt to start it.

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

Appendix: source

Thrown at karate-core/src/main/java/io/karatelabs/process/ProcessHandle.java:144

    public static ProcessHandle create(ProcessConfig config) {
        return new ProcessHandle(config);
    }

    /**
     * Create and immediately start ProcessHandle.
     */
    public static ProcessHandle start(ProcessConfig config) {
        ProcessHandle handle = new ProcessHandle(config);
        handle.start();
        return handle;
    }

    /**
     * Start the process. Can only be called once.
     */
    public ProcessHandle start() {
        if (!started.compareAndSet(false, true)) {
            throw new RuntimeException("process already started");
        }
        try {
            java.lang.ProcessBuilder pb = new java.lang.ProcessBuilder(config.args());
            if (config.workingDir() != null) {
                pb.directory(config.workingDir().toFile());
            }
            if (!config.env().isEmpty()) {
                pb.environment().putAll(config.env());
            }
            pb.redirectErrorStream(config.redirectErrorStream());
            logger.debug("starting process: {}", config.args());
            this.process = pb.start();
            this.executor = Executors.newVirtualThreadPerTaskExecutor();
            LIVE_HANDLES.add(this);
            startStreamReaders();
            startExitWaiter();
            return this;
        } catch (Exception e) {

View on GitHub (pinned to a22eb90246)