microsoft/typescript-go · critical
SyncRpcChannel: could not obtain pipe file descriptors.
Error message
SyncRpcChannel: could not obtain pipe file descriptors.
What it means
On POSIX, SyncRpcChannel reuses the child's stdio pipe file descriptors for synchronous I/O by reading them off Node's internal stream handles (stdout._handle.fd / stdin._handle.fd). If those internals are missing or negative — a non-Node/edge runtime, an unusual Node build, or stdio configured differently than the code expects — synchronous readSync/writeSync would be impossible, so the channel destroys the streams, kills the child, and throws.
Source
Thrown at _packages/native-preview/src/api/syncChannel.ts:195
this.pipeFd = fd;
}
else {
// POSIX: use stdio pipe file descriptors directly.
this.child = spawn(exe, args, {
stdio: ["pipe", "pipe", "inherit"],
});
const stdout = this.child.stdout! as StdoutWithHandle;
const stdin = this.child.stdin! as StdinWithHandle;
this.readFd = stdout._handle.fd;
this.writeFd = stdin._handle.fd;
if (typeof this.readFd !== "number" || this.readFd < 0 || typeof this.writeFd !== "number" || this.writeFd < 0) {
stdout.destroy();
stdin.destroy();
this.child.kill();
throw new Error(
"SyncRpcChannel: could not obtain pipe file descriptors.",
);
}
// Set the pipe handles to blocking mode. Under node --test's
// process isolation, pipes are created in non-blocking mode
// (for the IPC channel). This causes readSync/writeSync to get
// EAGAIN, requiring costly 1ms sleeps per retry. Setting
// blocking mode ensures readSync blocks properly until data
// arrives, matching the behavior of the native libsyncrpc.
stdout._handle.setBlocking?.(true);
stdin._handle.setBlocking?.(true);
// Prevent Node's event-loop from reading stdout or keeping the
// process alive – we will use fs.readSync exclusively.
stdout.pause();
stdout.unref();
stdin.unref();
View on GitHub (pinned to 1bcfa18d79)
Solutions
- Run the sync client under a standard Node.js runtime
- If embedding, ensure child stdio remains real pipes and Node stream handles are untouched
- Fall back to the async API client, which uses ordinary stream I/O and does not need raw fds
Defensive patterns
Strategy: fallback
Validate before calling
const supportsRawFds = () => {
try { const c = spawn(process.execPath, ["-e", ""]]); return typeof (c.stdout?._handle as { fd?: number } | undefined)?.fd === "number"; } catch { return false; }
}; Try / catch
try { api = new API({ cwd }); } catch (e) { if ((e as Error).message.includes("could not obtain pipe file descriptors")) { /* switch to async API client which uses stream I/O */ } else throw e; } Prevention
- Run the sync client only under standard Node.js
- Do not replace or wrap child stdio streams in the host process
- Fall back to the async client on runtimes without Node stream handle internals
When it happens
Trigger: Running the sync client under a runtime without Node-compatible stream internals (Bun/Deno compatibility layers, patched Node, Electron with modified stdio); stdio fds already closed or remapped (fd 0/1 redirected to non-pipe files); sandboxed environments stripping handle objects.
Common situations: Bundling the sync client into Electron or a custom Node fork; test runners that replace process.stdout; environments where the spawn options in this code path get stdio other than ['pipe','pipe','inherit'].
Related errors
- ${method} returned null symbol for ${source.constructor.name
- ${method} returned no type for ${source.constructor.name} ${
- ${method} returned null signature for ${source.constructor.n
- Symbol ${data.id} references unknown canonical project '${da
- Child process exited with code ${this.child.exitCode} before
AI-assisted analysis of microsoft/typescript-go@1bcfa18d79 (2026-08-16).
Data as JSON: /api/errors/055efe161d5d161a.
Report an issue: GitHub.