BoundaryML/baml · info · LspError

Client closed

Error message

Client closed

What it means

LspError::ClientClosed signals that the client's outbound sink is gone, so the server can no longer deliver messages. thiserror renders the fixed message 'Client closed'. It is an expected lifecycle condition, not a bug in message handling.

Source

Thrown at baml_language/crates/baml_lsp/src/error.rs:22

/// Every failure a request or notification handler can report.
///
/// Serialized to the wire exclusively through [`LspError::to_response_error`];
/// the legacy `-32001 UnknownErrorCode` is never emitted.
#[derive(Debug, thiserror::Error)]
pub enum LspError {
    #[error("{0}")]
    NotificationExtractError(lsp_server::ExtractError<lsp_server::Notification>),
    #[error("Notification not supported: {0}")]
    NotificationNotSupported(String),
    #[error("{0}")]
    RequestExtractError(lsp_server::ExtractError<lsp_server::Request>),
    #[error("Request not supported: {0}")]
    RequestNotSupported(String),
    #[error("Failed to serialize request result: {0}")]
    RequestSerializeError(serde_json::Error),
    /// The client's sink is gone; nothing more can be delivered.
    #[error("Client closed")]
    ClientClosed,
    /// Bounded transport backpressure (LSP `RequestFailed`, `-32803`).
    #[error("LSP outbound sink is saturated")]
    OutboundSaturated,
    /// A frame larger than the transport limit (LSP `RequestFailed`,
    /// `-32803`).
    #[error("LSP outbound frame exceeds the transport limit")]
    OutboundOversized,
    #[error("Invalid command arguments for command: {command}: {message}")]
    InvalidCommandArguments { command: String, message: String },
    #[error("File not found: {}", .0.display())]
    FileNotFound(PathBuf),
    #[error("Path is invalid: {}: {message}", path.display())]
    InvalidPath { path: PathBuf, message: String },
    /// The document's path is under no known source root.
    #[error("No source root contains {}", .0.display())]
    NoRootForPath(PathBuf),
    /// Cancellation claimed the response while the request was queued or

View on GitHub (pinned to bd85ce9dee)

Solutions

  1. Treat this as normal shutdown: cancel outstanding work and exit the handler loop
  2. Check sink/connection liveness before dispatching long-running work
  3. Log at info/debug level rather than surfacing it as a user-facing failure

Example fix

// before
send(response)?; // Err(ClientClosed) bubbles up
// after
if let Err(LspError::ClientClosed) = send(response) {
    log::info!("client disconnected; dropping response");
    return Ok(());
}
Defensive patterns

Strategy: try-catch

Type guard

fn is_client_closed(e: &LspError) -> bool {
    matches!(e, LspError::ClientClosed)
}

Try / catch

if let Err(LspError::ClientClosed) = sink.send(msg) {
    log::info!("client gone; aborting send loop");
    return Ok(());
}

Prevention

When it happens

Trigger: Writing a response or notification after the client disconnected / the connection loop ended; sending on a closed channel backing the LSP sink during shutdown.

Common situations: Editor closed or LSP client killed while a long-running request (formatting, diagnostics) is still in flight; slow handler finishing after shutdown; tests tearing down the transport early.

Related errors


AI-assisted analysis of BoundaryML/baml@bd85ce9dee (2026-09-12). Data as JSON: /api/errors/43e0ee9481bf097e. Report an issue: GitHub.