BoundaryML/baml · error

LSP client sent exit before completing shutdown

Error message

LSP client sent exit before completing shutdown

What it means

Raised in `run_stdio_loop` when the LSP client sends the `exit` notification before a `shutdown` request completed. Per the LSP lifecycle, `exit` after `shutdown` is clean; `exit` before completed shutdown is an abnormal termination, so the server bails with a nonzero exit for stdio.

Source

Thrown at baml_language/crates/baml_lsp_server/src/lib.rs:921

                    break;
                }
                SubmitResult::Closed => {
                    terminate = true;
                    break;
                }
            }
        }
        if terminate || stdio_closed.load(std::sync::atomic::Ordering::Acquire) {
            break;
        }
    }

    runtime.close_session(stdio_session);

    if abnormal_exit {
        // Lifecycle rule: `exit` before a completed shutdown is an abnormal
        // termination (nonzero for stdio).
        anyhow::bail!("LSP client sent exit before completing shutdown");
    }

    tracing::info!("LSP server shutting down");
    Ok(())
}

fn absolutize_workspace_roots(workspace_roots: Vec<PathBuf>) -> anyhow::Result<Vec<PathBuf>> {
    if workspace_roots.iter().all(|root| root.is_absolute()) {
        return Ok(workspace_roots);
    }

    let cwd = std::env::current_dir().context("Failed to read current directory")?;
    Ok(workspace_roots
        .into_iter()
        .map(|root| {
            if root.is_absolute() {
                root
            } else {

View on GitHub (pinned to bd85ce9dee)

Solutions

  1. Fix the client to send `shutdown` (request) and wait for its response before sending `exit`
  2. If you control the editor plugin, implement the full LSP lifecycle: initialize → shutdown → exit
  3. Check the client's shutdown timeout settings and increase if it aborts mid-shutdown
  4. Inspect server logs to confirm `shutdown` was received before `exit`

Example fix

// before (client): fire-and-forget exit
connection.exit();

// after (client): complete the lifecycle handshake
let _ = connection.request("shutdown", null).await;
connection.notify("exit");
Defensive patterns

Strategy: try-catch

Try / catch

// client side: only send exit after shutdown resolved
match shutdown_result {
    Ok(_) => connection.notify("exit"),
    Err(e) => log::error!("shutdown failed, skipping exit: {e}"),
}

Prevention

When it happens

Trigger: Client sends the `exit` notification while a `shutdown` request is still pending or was never sent; a client crash or hard kill issuing `exit` mid-shutdown; a client bug skipping the `shutdown` request.

Common situations: Editors killing the language server process on close instead of performing the full shutdown/exit handshake; flaky LSP client plugins that fire `exit` immediately; timeouts in the client causing it to abandon shutdown.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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