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

  1. Treat the ORIGINAL panic (second %v in the message) as the root cause and chase its stack, not the write failure
  2. Fix or guard the panic inside the request handler
  3. Keep the peer's connection open until the final response for every request it sent has been written, or drain gracefully on shutdown
  4. 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

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.