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
- Remove the 'ipc' entry for synchronous spawns: stdio: ['pipe', 'pipe', 'pipe']
- If you need message passing, use the async fork()/spawn() with an event loop
- 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
- spawnSync/forkSync can never carry an IPC channel — design sync flows around stdin/stdout
- Keep one stdio preset per mode (sync vs async) rather than sharing it
- Switch to async fork() as soon as messaging is a requirement
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
- ERR_IPC_ONE_PIPE
- ERR_INVALID_SYNC_FORK_INPUT
- ERR_INVALID_ARG_VALUE
- ERR_CHILD_PROCESS_IPC_REQUIRED
- ERR_INVALID_ARG_TYPE
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/a18f3682cac81ab1.
Report an issue: GitHub.