denoland/deno · error · Error

ERR_IPC_ONE_PIPE

ERR_IPC_ONE_PIPE

Error message

Child process can have only one IPC pipe

What it means

ERR_IPC_ONE_PIPE is thrown when the normalized stdio array contains the 'ipc' entry more than once. A child process can have exactly one IPC channel; each 'ipc' slot maps to the single kIpc option handed to the Rust spawn layer, so a second one is rejected before process creation.

Source

Thrown at ext/node/polyfills/internal/child_process.ts:435

    const argv0 = args.length > 0 ? args[0] : command;
    const builtCommand = buildCommand(
      command,
      ArrayPrototypeSlice(args, 1),
      env,
    );
    const cmd = builtCommand[0];
    const cmdArgs = builtCommand[1];
    const includeNpmProcessState = builtCommand[2];

    this.spawnfile = cmd;
    this.spawnargs = [cmd, ...new SafeArrayIterator(cmdArgs)];

    const ipc = ArrayPrototypeIndexOf(normalizedStdio, "ipc");
    if (
      ipc !== -1 &&
      ArrayPrototypeIndexOf(normalizedStdio, "ipc", ipc + 1) !== -1
    ) {
      throw new ERR_IPC_ONE_PIPE();
    }

    const extraStdioOffset = 3; // stdin, stdout, stderr

    const extraStdioNormalized = [];
    for (let i = 0; i < extraStdio.length; i++) {
      const fd = i + extraStdioOffset;
      if (fd === ipc) {
        // IPC fd is handled separately in Rust via the kIpc option.
        // Push a placeholder so the array indices stay aligned with
        // fd numbers, but don't double-push.
        ArrayPrototypePush(extraStdioNormalized, "null");
        continue;
      }
      ArrayPrototypePush(extraStdioNormalized, toDenoStdio(extraStdio[i]));
    }

    // Windows does not support uid/gid options - throw ENOTSUP synchronously.

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Include 'ipc' exactly once in the stdio array, e.g. ['ipc', 'pipe', 'pipe']
  2. Prefer fork() which manages the IPC entry for you — don't also hand it a manual 'ipc'
  3. Dedupe before spawn: new Set(stdio).has('ipc') count check, keep only the first occurrence

Example fix

// before
spawn(process.execPath, [script], { stdio: ['ipc', 'pipe', 'pipe', 'ipc'] });

// after
spawn(process.execPath, [script], { stdio: ['ipc', 'pipe', 'pipe'] });
Defensive patterns

Strategy: validation

Validate before calling

function withSingleIpc(stdio) {
  const first = stdio.indexOf('ipc');
  if (first === -1) return stdio;
  if (stdio.indexOf('ipc', first + 1) !== -1) {
    throw new Error('stdio may contain at most one \'ipc\' entry');
  }
  return stdio;
}

Type guard

const hasSingleIpc = (stdio) => stdio.filter((s) => s === 'ipc').length <= 1;

Try / catch

try { spawn(cmd, args, { stdio }); } catch (err) {
  if (err?.code === 'ERR_IPC_ONE_PIPE') {
    const first = stdio.indexOf('ipc');
    spawn(cmd, args, { stdio: stdio.map((s, i) => s === 'ipc' && i !== first ? 'pipe' : s) });
  }
}

Prevention

When it happens

Trigger: spawn(cmd, args, { stdio: ['ipc', 'ipc', 'pipe'] }); manually adding 'ipc' to stdio while also passing options.serialization/stdio handling that already includes it; fork()-style usage where a channel is appended on top of a stdio array that already contains 'ipc'.

Common situations: Copy-pasting a fork stdio preset like ['ipc', 'pipe', 'pipe', 'ipc'] (an extra entry for fd 3); merging user stdio with IPC defaults via array concat producing duplicates; upgrading code from spawn() to fork() without removing the manual 'ipc' entry.

Related errors


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