Skip to content

throwOnError does not throw when the child is killed by a signal #144

@wuservices

Description

@wuservices

Description

With throwOnError: true, a child process that exits with a nonzero code throws NonZeroExitError as expected — but a child killed by a signal (SIGABRT, SIGKILL, SIGSEGV, …) resolves normally, as if it succeeded.

Repro

import { x } from 'tinyexec'

// Child dies by signal — resolves normally, exitCode: undefined
const r = await x('node', ['-e', 'process.kill(process.pid, "SIGABRT")'], {
  throwOnError: true,
  nodeOptions: { stdio: 'inherit' },
})
console.log('returned normally — exitCode:', r.exitCode) // undefined

// Child exits nonzero — throws NonZeroExitError as expected
await x('node', ['-e', 'process.exit(2)'], {
  throwOnError: true,
  nodeOptions: { stdio: 'inherit' },
}) // throws ✓

Reproduced on 1.2.2 and present on current main.

Root cause

When a child dies by signal, Node sets child.exitCode = null (and signalCode to the signal). The exitCode getter only forwards non-null codes, so signal death yields undefined:

https://github.com/tinylibs/tinyexec/blob/main/src/main.ts#L145-L147

…and the throwOnError condition explicitly excludes undefined:

this._options?.throwOnError && this.exitCode !== 0 && this.exitCode !== undefined

(The undefined exclusion presumably exists for the not-yet-exited case, but post-exit it also matches every signal-killed child.)

Real-world impact

Nuxt's nuxi typecheck spawns vue-tsc through tinyexec with throwOnError: true. When vue-tsc hits a V8 OOM (FATAL ERROR: Ineffective mark-compacts near heap limit → SIGABRT), nuxi typecheck exits 0 — our CI reported a green typecheck on a run where the type checker had crashed without checking anything. Any tool that trusts throwOnError for correctness has the same exposure: crashes read as success.

Suggested fix

After the child has exited, treat signal termination as failure under throwOnError — e.g. throw when exitCode !== 0 || process.signalCode != null (post-exit), with the signal name in the error message. Related: #28 (exposing the killing signal would make the error message straightforward).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions