microsoft/typescript-go · error

SyncRpcChannel is closed

Error message

SyncRpcChannel is closed

What it means

Every request goes through ensureOpen(), which throws 'SyncRpcChannel is closed' once readFd is negative — the sentinel set by close() (and the cleanup path when the child dies). The channel is single-use: after closing (explicitly or because the child terminated), any further apiRequest/request call fails fast instead of writing to dead file descriptors.

Source

Thrown at _packages/native-preview/src/api/syncChannel.ts:272

            }
            // Destroy the stdio streams so that their pipe handles are closed
            // and no longer prevent the event loop from draining.
            this.child.stdout?.destroy();
            this.child.stdin?.destroy();
            this.child.kill();
            this.readFd = -1;
            this.writeFd = -1;
        }
        catch {
            // swallow – process may already be dead
        }
    }

    // ── Core request loop ───────────────────────────────────────────

    private ensureOpen(): void {
        if (this.readFd < 0) {
            throw new Error("SyncRpcChannel is closed");
        }
    }

    private getMethodBuf(method: string): Buffer {
        let buf = this.methodBufCache.get(method);
        if (buf === undefined) {
            buf = Buffer.from(method, "utf-8");
            this.methodBufCache.set(method, buf);
        }
        return buf;
    }

    private requestBytesSync(method: string, payload: Buffer | Uint8Array | string): Buffer {
        const methodBuf = this.getMethodBuf(method);
        if (this.collectTiming) {
            this.lastBytesSent = typeof payload === "string"
                ? Buffer.byteLength(payload, "utf-8")
                : payload.length;

View on GitHub (pinned to 1bcfa18d79)

Solutions

  1. Track closed state in your own wrapper and skip or re-create requests after close()
  2. Serialize close() with pending requests (same tick ordering) since the channel is synchronous
  3. Recreate the Client/API instance if you need to keep working after a deliberate close

Example fix

// before
api.close();
api.getProjectSnapshot("tsconfig.json"); // channel closed -> throws

// after
api.close();
api = new API({ cwd }); // fresh instance
api.getProjectSnapshot("tsconfig.json");
Defensive patterns

Strategy: validation

Validate before calling

class SafeApi { constructor(private api: API) {}
 private closed = false;
 close() { this.closed = true; this.api.close(); }
 getProjectSnapshot(p: string) {
   if (this.closed) throw new Error("API already closed; recreate it");
   return this.api.getProjectSnapshot(p);
 } }

Try / catch

try { result = api.someMethod(); } catch (e) { if ((e as Error).message === "SyncRpcChannel is closed") { api = new API({ cwd }); result = api.someMethod(); } else throw e; }

Prevention

When it happens

Trigger: Calling any API method after client.close(); issuing requests after the tsgo child crashed and the channel cleaned up; a request racing close() from another synchronous entry point.

Common situations: Editor shutdown ordering bugs (dispose runs while queued queries still execute); forgetting that API.close() cascades to the channel; long-lived singletons that get closed on error then reused.

Related errors


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