{"record":{"id":"e9aa49c368db4276","repo":"denoland/deno","slug":"child-process-has-already-terminated","errorCode":null,"errorMessage":"Child process has already terminated","messagePattern":"Child process has already terminated","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"ext/process/40_process.js","lineNumber":572,"sourceCode":"      signal: status.signal,\n      get stdout() {\n        if (stdout == null) {\n          throw new TypeError(\"Cannot get 'stdout': 'stdout' is not piped\");\n        }\n        return stdout;\n      },\n      get stderr() {\n        if (stderr == null) {\n          throw new TypeError(\"Cannot get 'stderr': 'stderr' is not piped\");\n        }\n        return stderr;\n      },\n    };\n  }\n\n  kill(signo = \"SIGTERM\") {\n    if (this.#waitComplete) {\n      throw new TypeError(\"Child process has already terminated\");\n    }\n    op_spawn_kill(this.#rid, signo);\n  }\n\n  async [SymbolAsyncDispose]() {\n    try {\n      op_spawn_kill(this.#rid, \"SIGTERM\");\n    } catch {\n      // ignore errors from killing the process (such as ESRCH or BadResource)\n    }\n    await this.#status;\n  }\n\n  ref() {\n    core.refOpPromise(this.#waitPromise);\n    if (this.#stdout) readableStreamForRidUnrefableRef(this.#stdout);\n    if (this.#stderr) readableStreamForRidUnrefableRef(this.#stderr);\n    if (!this.#waitComplete) {","sourceCodeStart":554,"sourceCodeEnd":590,"githubUrl":"https://github.com/denoland/deno/blob/9ad36f7a2cce60488e6ec52283efb32efddaf93a/ext/process/40_process.js#L554-L590","documentation":"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.","triggerScenarios":"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.","commonSituations":"Race between a timeout-kill and normal process exit; cleanup/finally blocks that kill unconditionally; supervisors that signal on shutdown even for finished children.","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"],"exampleFix":"// before\nconst child = cmd.spawn();\nconst timer = setTimeout(() => child.kill(), 1000);\nconst out = await child.output();\nchild.kill(\"SIGTERM\"); // TypeError: already terminated\n\n// after\nconst child = cmd.spawn();\nconst timer = setTimeout(() => child.kill(), 1000);\nconst out = await child.output();\nclearTimeout(timer); // process finished; no late kill","handlingStrategy":"try-catch","validationCode":"let exited = false;\nconst child = cmd.spawn();\nchild.status.then(() => { exited = true; }); // or: child.output().then(() => exited = true)\nfunction safeKill(signal?: Deno.Signal) {\n  if (!exited) child.kill(signal);\n}","typeGuard":"const isAlive = (child: Deno.ChildProcess, exited: boolean): boolean => !exited;","tryCatchPattern":"try { child.kill(\"SIGTERM\"); } catch (err) { if (err instanceof TypeError && err.message.includes(\"already terminated\")) { /* already exited; ignore */ } else throw err; }","preventionTips":["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"],"tags":["deno","child-process","kill","signal","process-exit","race-condition"],"backgroundTag":"process-already-exited","analyzedSha":"9ad36f7a2cce60488e6ec52283efb32efddaf93a","analyzedAt":"2026-08-20T13:07:44.778Z","contentChangedAt":"2026-08-20T13:07:44.778Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}