Hmbown/CodeWhale · warning

LSP diagnostics timed out waiting for another document

Error message

LSP diagnostics timed out waiting for another document

What it means

`diagnostics_for` serializes open/version/send/wait transactions behind a mutex because one receiver cannot serve concurrent polling. If the gate cannot be acquired within `wait`, this error fires instead of risking interleaved reads on the diagnostics channel.

Solutions

  1. Serialize diagnostics requests in the caller instead of issuing them concurrently
  2. Increase the wait budget for the gate acquisition
  3. Add per-request timeout so a stuck transaction cannot starve the gate
  4. Use separate transports/channels per document if concurrency is required

Example fix

// before
let handles = files.iter().map(|f| tokio::spawn(diagnostics_for(f)));
// after
for f in &files {
    diagnostics_for(f).await?; // one transaction at a time
}
Defensive patterns

Strategy: try-catch

Validate before calling

// track in-flight diagnostics per transport before calling
if (diagnosticsInFlight.has(transportId)) await diagnosticsInFlight.get(transportId);

Try / catch

match diagnostics_for(path, text, wait).await {
    Err(e) if e.to_string().contains("waiting for another document") => {
        eprintln!("diagnostics busy; retry after current document completes");
    }
    other => other,
}

Prevention

When it happens

Trigger: A second diagnostics request arrives while another document's wait transaction still holds `diagnostics_gate` and the first one does not release it within `wait`.

Common situations: UI polling diagnostics for many open files concurrently with a short wait budget; a previous diagnostics wait stuck on a hung server.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/d87a80ed87e0c4fc. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/lsp/client.rs:387

impl LspTransport for StdioLspTransport {
    fn is_alive(&self) -> bool {
        // stderr may close independently. The writer, reader and dispatcher
        // are the protocol lifetime; none may have exited or been aborted.
        !self.tx_outbound.is_closed() && self.tasks.iter().skip(1).all(|task| !task.is_finished())
    }

    async fn diagnostics_for(
        &self,
        path: &Path,
        text: &str,
        wait: Duration,
    ) -> Result<DiagnosticPublication> {
        // One receiver cannot serve concurrent polling safely: serialize the
        // open/version/send/wait transaction, including semantic ensure_open.
        let deadline = tokio::time::Instant::now() + wait;
        let _gate = timeout(wait, self.diagnostics_gate.lock())
            .await
            .map_err(|_| anyhow!("LSP diagnostics timed out waiting for another document"))?;
        let path_buf = path.to_path_buf();
        let (_, version) = timeout(
            deadline.saturating_duration_since(tokio::time::Instant::now()),
            self.open_or_change(path, text),
        )
        .await
        .map_err(|_| anyhow!("LSP diagnostics timed out sending document"))??;
        loop {
            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
            if remaining.is_zero() {
                return Err(anyhow!(
                    "LSP diagnostics timed out before a current publication"
                ));
            }
            let mut rx = self.diagnostics_rx.lock().await;
            let (file, published_version, items) = match timeout(remaining, rx.recv()).await {
                Ok(Some(item)) => item,
                Ok(None) => {

View on GitHub (pinned to 73e0f67d83)