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
- Call start()/fork() only once per ProcessHandle; store the returned handle and reuse it
- If you need a restart, create a new ProcessHandle with the same config instead of re-starting
- 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
- Start each handle exactly once at construction; share the started handle
- Create a new ProcessHandle per restart instead of reusing
- Document single-start contract at the call site
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
- context.getGlobal() requires a name argument
- context.setGlobal() requires a name argument
- error in waitForOutput
- error waiting for process
- exec() needs at least one argument
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)