BoundaryML/baml · warning · anyhow

Server received an exit notification before a shutdown…

Error message

Server received an exit notification before a shutdown request was sent. Exiting...

What it means

Per the LSP specification, a client must send a 'shutdown' request before the 'exit' notification. If handle_shutdown sees an exit notification without a preceding shutdown request, it bails with this error to terminate the main loop, flagging a protocol violation by the client.

Solutions

  1. Ensure the client sends request 'shutdown' followed by notification 'exit' per LSP spec.
  2. If abrupt exit is acceptable, treat exit-without-shutdown as a normal termination instead of an error in the server.
  3. Check the client extension for code paths that skip shutdown (crash handlers, force-kill timers).
  4. Log at info/warn level and exit cleanly rather than propagating an anyhow bail.

Example fix

// before
anyhow::bail!("Server received an exit notification before a shutdown request was sent. Exiting...");
// after
log::warn!("Client sent exit without shutdown; exiting.");
return Ok(true); // stop the event loop without error
Defensive patterns

Strategy: try-catch

Validate before calling

// ensure notification payload matches the expected type before sending
JSON.stringify(notification.params); // catch non-serializable payloads early

Type guard

function hasValidParams(n: { params?: unknown }): boolean {
  return n.params !== undefined;
}

Try / catch

match notification.extract::<N>(method) {
    Ok(n) => handle(n),
    Err(ExtractError::JsonError(e)) => log::error!("bad notification params: {e}"),
    Err(_) => unreachable!(),
}

Prevention

When it happens

Trigger: The client sends notification 'exit' without first sending the 'shutdown' request — e.g. the editor process is killed, the extension crashes mid-shutdown, or a custom/test client omits shutdown.

Common situations: Editors force-killing the server on window close, flaky client extensions shutting down abruptly, integration tests that only send exit, or race conditions in client teardown logic.

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/39770d82e774a282. Report an issue: GitHub.

Appendix: source

Thrown at engine/language_server/src/server/connection.rs:139

                            );
                            self.sender.send(lsp::Message::Response(lsp::Response::new_err(
                                id.clone(),
                                lsp::ErrorCode::InvalidRequest as i32,
                                "Server received unexpected request while waiting for exit notification".to_string(),
                            )))?;
                        }
                        message => {
                            tracing::warn!(
                                "Server received unexpected message while waiting for exit notification: {message:?}"
                            );
                        }
                    }
                }
            }
            lsp::Message::Notification(lsp::Notification { method, .. })
                if method == lsp_types::notification::Exit::METHOD =>
            {
                anyhow::bail!("Server received an exit notification before a shutdown request was sent. Exiting...");
            }
            _ => Ok(false),
        }
    }

    /// Join the I/O threads that underpin this connection.
    /// This is guaranteed to be nearly immediate since
    /// we close the only active channels to these threads prior
    /// to joining them.
    pub fn close(self) -> anyhow::Result<()> {
        std::mem::drop(
            Arc::into_inner(self.sender)
                .expect("the client sender shouldn't have more than one strong reference"),
        );
        std::mem::drop(self.receiver);
        self.threads.into_iter().try_for_each(|t| t.join())?;
        Ok(())
    }

View on GitHub (pinned to bd85ce9dee)