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

  1. Check whether the client is still alive when this fires; if the editor exited, this error is expected shutdown noise, not a bug
  2. 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
  3. Ensure custom handler return values only contain lsproto-generated, JSON-tagged structs
  4. 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

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


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