denoland/deno · error · NodeError

ERR_IPC_SYNC_FORK

ERR_IPC_SYNC_FORK

Error message

IPC cannot be used with synchronous forks

What it means

getValidStdio() throws ERR_IPC_SYNC_FORK when a stdio entry equals 'ipc' while the call is on the synchronous path (sync === true, i.e. spawnSync/forkSync internals). An IPC channel needs the parent's event loop to pump messages; a synchronous spawn returns only after the child exits, so Node (and this polyfill) forbid the combination up front.

Source

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

    stdio = [stdio, stdio, stdio];
  } else if (!ArrayIsArray(stdio)) {
    throw new ERR_INVALID_ARG_VALUE("stdio", stdio);
  }

  // Expand stdio array to at least 3 elements (mutates the input array)
  while (stdio.length < 3) {
    ArrayPrototypePush(stdio, undefined);
  }

  // Process each stdio element
  const result = [];

  for (let i = 0; i < stdio.length; i++) {
    const value = stdio[i];

    if (value === "ipc") {
      if (sync) {
        throw new ERR_IPC_SYNC_FORK();
      }
      ipc = i;
      ipcFd = i;
      ArrayPrototypePush(result, { type: "ipc" });
    } else if (value === "ignore" || value === null) {
      ArrayPrototypePush(result, { type: "ignore" });
    } else if (value === "pipe" || value === undefined) {
      ArrayPrototypePush(result, { type: "pipe" });
    } else if (value === "inherit") {
      ArrayPrototypePush(result, { type: "inherit" });
    } else if (value === "overlapped") {
      ArrayPrototypePush(result, { type: "overlapped" });
    } else if (typeof value === "number") {
      ArrayPrototypePush(result, { type: "fd", fd: value });
    } else if (typeof value === "string") {
      // Invalid string value
      throw new ERR_INVALID_SYNC_FORK_INPUT(value);
    } else if (typeof value === "object" && value !== null) {

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Remove the 'ipc' entry for synchronous spawns: stdio: ['pipe', 'pipe', 'pipe']
  2. If you need message passing, use the async fork()/spawn() with an event loop
  3. Replace IPC in sync code with stdin/stdout pipes: write input, then read result.stdout

Example fix

// before
const r = spawnSync(process.execPath, [script], { stdio: ['ipc', 'pipe', 'pipe'] });

// after
const r = spawnSync(process.execPath, [script], { stdio: ['pipe', 'pipe', 'pipe'], input: JSON.stringify(msg) });
const reply = JSON.parse(r.stdout.toString());
Defensive patterns

Strategy: validation

Validate before calling

if (sync && Array.isArray(stdio) && stdio.includes('ipc')) {
  throw new Error('IPC is not supported by spawnSync; use async fork()/spawn()');
}
const safeStdio = stdio.map((s) => (s === 'ipc' ? 'pipe' : s));

Type guard

const isSyncSafeStdio = (stdio) => !stdio.includes('ipc');

Try / catch

try { spawnSync(cmd, args, { stdio }); } catch (err) {
  if (err?.code === 'ERR_IPC_SYNC_FORK') {
    spawnSync(cmd, args, { stdio: stdio.map((s) => (s === 'ipc' ? 'pipe' : s)) });
  }
}

Prevention

When it happens

Trigger: spawnSync(cmd, args, { stdio: ['ipc', 'pipe', 'pipe'] }); forkSync(script) with an 'ipc' entry; any sync spawn where an 'ipc' stdio element survives normalization.

Common situations: Converting async fork() messaging code to spawnSync 'for determinism' in build scripts; copy-pasting a fork stdio preset into a sync call; CLI tools that must block until the child exits but still try to hold a message channel.

Related errors


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