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

  1. 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)
  2. Wrap kill() in try/catch and ignore this TypeError when best-effort cleanup is acceptable
  3. Use await using / Symbol.asyncDispose (Deno disposes the child with SIGTERM and waits safely) instead of manual kill in cleanup paths
  4. 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

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


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/e9aa49c368db4276. Report an issue: GitHub.