google/zx · error · Fail

The signal is controlled by another process.

Error message

The signal is controlled by another process.

What it means

Thrown by ProcessPromise.abort() when the AbortSignal in use is not zx's internal one (this.signal !== this.ac.signal). This happens when an external AbortSignal was supplied via the {signal:} option; zx cannot abort a controller it does not own, so abort() on the ProcessPromise is rejected.

Source

Thrown at src/core.ts:439

    ProcessPromise.bus.unpipeBack(this)
    if (output.ok || this.isNothrow()) {
      this._stage = 'fulfilled'
      this._resolve(output)
    } else {
      this._stage = 'rejected'
      if (legacy) {
        this._resolve(output) // to avoid unhandledRejection alerts
        throw output.cause || output
      }
      this._reject(output)
      if (this.sync) throw output
    }
  }

  abort(reason?: string) {
    if (this.isSettled()) throw new Fail('Too late to abort the process.')
    if (this.signal !== this.ac.signal)
      throw new Fail('The signal is controlled by another process.')
    if (!this.child)
      throw new Fail('Trying to abort a process without creating one.')

    this.ac.abort(reason)
  }

  kill(signal?: NodeJS.Signals | null): Promise<void> {
    if (this.isSettled()) throw new Fail('Too late to kill the process.')
    if (!this.child)
      throw new Fail('Trying to kill a process without creating one.')
    if (!this.pid) throw new Fail('The process pid is undefined.')

    return $.kill(this.pid, signal || this._snapshot.killSignal || $.killSignal)
  }

  // Configurators
  stdio(
    stdin: IOType | StdioOptions,

View on GitHub (pinned to 00a2c484e2)

Solutions

  1. Abort via your own controller: `ac.abort()` instead of `pp.abort()`.
  2. Drop the custom signal option so zx manages abort internally.
  3. Avoid sharing a single signal across processes if you need per-process abort control.

Example fix

// before
const ac = new AbortController()
const pp = $({ signal: ac.signal })`sleep 10`
pp.abort()
// after: abort the controller you own
ac.abort()
Defensive patterns

Strategy: validation

Validate before calling

// If you supplied an external signal, abort THAT controller instead.
const ownsSignal = (pp: ProcessPromise): boolean =>
  (pp as any).signal === (pp as any).ac.signal

if (ownsSignal(pp)) pp.abort()
else externalController.abort()

Try / catch

try {
  pp.abort()
} catch (e) {
  if (e instanceof Fail && /signal is controlled by another process/.test(e.message)) {
    externalController.abort() // abort the controller you own
  } else throw e
}

Prevention

When it happens

Trigger: `const ac = new AbortController(); $({signal: ac.signal})\`...\`.abort()` — the ProcessPromise's signal is the external ac.signal, not its internal controller, so pp.abort() throws.

Common situations: Sharing one AbortController across multiple commands or with fetch; integrating zx into a larger abort-coordination layer; passing a request-scoped signal.

Related errors


AI-assisted analysis of google/zx@00a2c484e2 (2026-08-13). Data as JSON: /api/errors/d7665e35f92346f4. Report an issue: GitHub.