denoland/deno · error · TypeError
Child process has already terminated
Error message
Child process has already terminated
What it means
ChildProcess.kill() throws a TypeError once the internal #waitComplete flag is set, which happens when the process's status promise settles (wait(), output(), or a previous kill/exit completed). After termination the child no longer exists, so signaling it again is rejected at the JS layer before op_spawn_kill runs.
Source
Thrown at ext/process/40_process.js:572
signal: status.signal,
get stdout() {
if (stdout == null) {
throw new TypeError("Cannot get 'stdout': 'stdout' is not piped");
}
return stdout;
},
get stderr() {
if (stderr == null) {
throw new TypeError("Cannot get 'stderr': 'stderr' is not piped");
}
return stderr;
},
};
}
kill(signo = "SIGTERM") {
if (this.#waitComplete) {
throw new TypeError("Child process has already terminated");
}
op_spawn_kill(this.#rid, signo);
}
async [SymbolAsyncDispose]() {
try {
op_spawn_kill(this.#rid, "SIGTERM");
} catch {
// ignore errors from killing the process (such as ESRCH or BadResource)
}
await this.#status;
}
ref() {
core.refOpPromise(this.#waitPromise);
if (this.#stdout) readableStreamForRidUnrefableRef(this.#stdout);
if (this.#stderr) readableStreamForRidUnrefableRef(this.#stderr);
if (!this.#waitComplete) {View on GitHub (pinned to 9ad36f7a2c)
Solutions
- Track process liveness: keep the awaited status/output result and skip kill() when it has settled (e.g. check a saved exit record before killing)
- Wrap kill() in try/catch and ignore this TypeError when best-effort cleanup is acceptable
- Use await using / Symbol.asyncDispose (Deno disposes the child with SIGTERM and waits safely) instead of manual kill in cleanup paths
- Clear timeout timers once the process exits so a late kill cannot fire
Example fix
// before
const child = cmd.spawn();
const timer = setTimeout(() => child.kill(), 1000);
const out = await child.output();
child.kill("SIGTERM"); // TypeError: already terminated
// after
const child = cmd.spawn();
const timer = setTimeout(() => child.kill(), 1000);
const out = await child.output();
clearTimeout(timer); // process finished; no late kill Defensive patterns
Strategy: try-catch
Validate before calling
let exited = false;
const child = cmd.spawn();
child.status.then(() => { exited = true; }); // or: child.output().then(() => exited = true)
function safeKill(signal?: Deno.Signal) {
if (!exited) child.kill(signal);
} Type guard
const isAlive = (child: Deno.ChildProcess, exited: boolean): boolean => !exited;
Try / catch
try { child.kill("SIGTERM"); } catch (err) { if (err instanceof TypeError && err.message.includes("already terminated")) { /* already exited; ignore */ } else throw err; } Prevention
- Clear kill timers as soon as the process exits
- Track settlement of status/output before sending signals
- Prefer await using (async dispose) over manual kill in cleanup paths
When it happens
Trigger: Calling child.kill() after awaiting child.output(), child.status, or child.completed; sending a second signal after an earlier kill() already terminated the process; timeout wrappers that fire after the process already exited normally.
Common situations: Race between a timeout-kill and normal process exit; cleanup/finally blocks that kill unconditionally; supervisors that signal on shutdown even for finished children.
Related errors
- Cannot get 'stdout': 'stdout' is not piped
- Cannot get 'stderr': 'stderr' is not piped
- Illegal constructor
- Cannot collect output: 'stdout' is locked
- Cannot collect output: 'stderr' is locked
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/e9aa49c368db4276.
Report an issue: GitHub.