microsoft/typescript-go · critical
failed to write message: %w
Error message
failed to write message: %w
What it means
Raised in the server's outbound write loop when s.w.Write(msg) fails while pushing a JSON-RPC message to the client transport. It means the underlying writer (stdio pipe or client connection) is broken, so no further messages can be delivered and the write loop terminates. A special case exists for messageMarshalError on responses: the server attempts to substitute an error response instead of failing, but every other write error (broken pipe, closed conn, marshal error on a request/notification) propagates as this wrapped error.
Source
Thrown at internal/lsp/server.go:649
func (s *Server) writeLoop(ctx context.Context) error {
for {
msg, err := s.outgoingQueue.Get(ctx)
if err != nil {
return err
}
if err := s.w.Write(msg); err != nil {
var marshalErr *messageMarshalError
if errors.As(err, &marshalErr) && msg.Kind == jsonrpc.MessageKindResponse {
if resp := msg.AsResponse(); resp.ID != nil && resp.Error == nil {
s.logger.Errorf("failed to marshal response for request %s: %v", resp.ID, marshalErr)
if sendErr := s.sendError(resp.ID, marshalErr); sendErr != nil {
return sendErr
}
continue
}
}
return fmt.Errorf("failed to write message: %w", err)
}
}
}
// WARNING: this should only be called in the async portion of a request handler,
// otherwise a deadlock can occur.
func sendClientRequest[Req, Resp any](ctx context.Context, s *Server, info lsproto.RequestInfo[Req, Resp], params Req) (Resp, error) {
id := jsonrpc.NewIDString(fmt.Sprintf("ts%d", s.clientSeq.Add(1)))
req := info.NewRequestMessage(id, params)
responseChan := make(chan *lsproto.ResponseMessage, 1)
s.pendingServerRequestsMu.Lock()
s.pendingServerRequests[*id] = responseChan
s.pendingServerRequestsMu.Unlock()
defer func() {
s.pendingServerRequestsMu.Lock()
defer s.pendingServerRequestsMu.Unlock()View on GitHub (pinned to 1bcfa18d79)
Solutions
- Check whether the client is still alive when this fires; if the editor exited, this error is expected shutdown noise, not a bug
- If it is a marshal failure, reproduce with logging: the code already logs 'failed to marshal response for request %s' before retrying with sendError; capture that log to identify the offending field
- Ensure custom handler return values only contain lsproto-generated, JSON-tagged structs
- If the transport closes by design, drain/ignore the error after shutdown is initiated instead of treating it as fatal
Example fix
// before
if err := s.w.Write(msg); err != nil {
return fmt.Errorf("failed to write message: %w", err)
}
// after (ignore write failures once shutdown has begun)
if err := s.w.Write(msg); err != nil {
if s.isShutdown() {
s.logger.Warn("write after shutdown:", err)
continue
}
return fmt.Errorf("failed to write message: %w", err)
} Defensive patterns
Strategy: try-catch
Try / catch
if err := s.w.Write(msg); err != nil {
var marshalErr *messageMarshalError
if errors.As(err, &marshalErr) && canSynthesizeErrorFor(msg) {
// route through sendError replacement, keep loop alive
continue
}
if s.shutdownStarted() {
return nil // client went away during shutdown - not a defect
}
return fmt.Errorf("failed to write message: %w", err)
} Prevention
- Treat any error containing 'failed to write message' after client exit as expected shutdown noise
- Keep handler response types limited to lsproto-generated structs to avoid marshal errors
- Add a shutdown flag checked in the write loop before returning the error
When it happens
Trigger: Client process crashes or closes stdin/stdout while the server is mid-response; LSP client cancels and tears down the connection during a large hover/completion payload; a response contains types the marshaler cannot serialize and msg is not a response with a settable Error field; malformed lsproto struct handed to Write from a custom handler.
Common situations: Editor window closed during a long-running diagnostics pass; wrapper (node/npm shim) between editor and server dying; pipe buffer backpressure combined with client exit; version mismatch where a newly added response field is a non-marshalable Go type.
Related errors
- api: failed to write server timing response: %v
- api: failed to write reset server timing response: %v
- _submodules/TypeScript does not exist; try running `git subm
- Cannot parse string enum value: ${goValue}
- No members found for enum ${def.name} in ${def.goFile}
AI-assisted analysis of microsoft/typescript-go@1bcfa18d79 (2026-08-16).
Data as JSON: /api/errors/622563cfb12ed748.
Report an issue: GitHub.