microsoft/typescript-go · critical
api: failed to write panic error response: %v (original pani
Error message
api: failed to write panic error response: %v (original panic: %v)
What it means
Raised inside the deferred recover of SyncConn.handleRequest when a request handler panicked AND the follow-up attempt to write the JSON-RPC error response to the client also failed. The connection is already unusable, so the code re-panics with both the write error and the original panic value, tearing down the connection's Run loop.
Source
Thrown at internal/api/conn_sync.go:131
if c.timing != nil {
start = time.Now()
}
// Recover from panics and convert to error response with stack trace
defer func() {
if r := recover(); r != nil {
stack := string(debug.Stack())
err = fmt.Errorf("panic: %v\n%s", r, stack)
c.mu.Lock()
writeErr := c.protocol.WriteError(msg.ID, &jsonrpc.ResponseError{
Code: jsonrpc.CodeInternalError,
Message: err.Error(),
})
c.mu.Unlock()
if writeErr != nil {
panic(fmt.Sprintf("api: failed to write panic error response: %v (original panic: %v)", writeErr, r))
}
}
}()
result, err = c.handler.HandleRequest(ctx, msg.Method, msg.Params)
if c.timing != nil {
c.timing.record(msg.Method, time.Since(start))
}
c.mu.Lock()
defer c.mu.Unlock()
var writeErr error
if err != nil {
writeErr = c.protocol.WriteError(msg.ID, &jsonrpc.ResponseError{
Code: jsonrpc.CodeInternalError,
Message: err.Error(),View on GitHub (pinned to 1bcfa18d79)
Solutions
- Treat the ORIGINAL panic (second %v in the message) as the root cause and chase its stack, not the write failure
- Fix or guard the panic inside the request handler
- Keep the peer's connection open until the final response for every request it sent has been written, or drain gracefully on shutdown
- If you embed SyncConn, wrap Run with a recover so this becomes a logged connection teardown instead of a process crash
Example fix
// before
func main() { conn.Run(ctx) } // panic kills the process
// after
func main() {
defer func() { if r := recover(); r != nil { log.Printf("conn crashed: %v", r) } }()
conn.Run(ctx)
} Defensive patterns
Strategy: try-catch
Try / catch
// Wrap the connection loop; a panic here always means the connection is dead.
func serve(conn *api.SyncConn, ctx context.Context) error {
defer func() {
if r := recover(); r != nil {
log.Printf("connection terminated by panic: %v", r)
}
}()
return conn.Run(ctx)
} Prevention
- Keep the transport open until every in-flight request has been answered
- Wrap SyncConn.Run in a recover so a broken pipe plus handler panic logs instead of killing the process
- Fix handler panics reported in the embedded stack before chasing the write failure
When it happens
Trigger: c.handler.HandleRequest panics (nil dereference, index out of range in project/ast code) while the peer has already closed the transport or the pipe is broken, so protocol.WriteError under c.mu returns EPIPE/EOF/use-of-closed-connection.
Common situations: Editor or LSP-style client disconnects (restart, crash, cancelled request) at the exact moment the server hits a bug; test harnesses closing the io.ReadWriteCloser before the last response is flushed; half-closed TCP connections in CI.
Related errors
AI-assisted analysis of microsoft/typescript-go@1bcfa18d79 (2026-08-16).
Data as JSON: /api/errors/dde1f6e6d67858c1.
Report an issue: GitHub.