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

  1. Run the sync client under a standard Node.js runtime
  2. If embedding, ensure child stdio remains real pipes and Node stream handles are untouched
  3. 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

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


AI-assisted analysis of microsoft/typescript-go@1bcfa18d79 (2026-08-16). Data as JSON: /api/errors/055efe161d5d161a. Report an issue: GitHub.