Hmbown/CodeWhale · error

LSP request channel closed

Error message

LSP request channel closed

What it means

The reply channel for a pending request closed before an answer arrived, which happens when the LSP reader task exits - normally because the server process died or closed its stdout. The request can never complete; this error distinguishes a dead server from the separate timeout case.

Source

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

            "params": params,
        });
        if let Err(err) = send_message(&self.tx_outbound, &payload).await {
            let mut pending = self.pending.lock().await;
            pending.remove(&id);
            return Err(err);
        }
        match timeout(wait, rx).await {
            Ok(Ok(reply)) => {
                if let Some(error) = reply.get("error") {
                    let message = error
                        .get("message")
                        .and_then(|v| v.as_str())
                        .unwrap_or("LSP request failed");
                    return Err(anyhow!("{message}"));
                }
                Ok(reply.get("result").cloned().unwrap_or(Value::Null))
            }
            Ok(Err(_)) => Err(anyhow!("LSP request channel closed")),
            Err(_) => {
                let mut pending = self.pending.lock().await;
                pending.remove(&id);
                Err(anyhow!("LSP request timed out for {method}"))
            }
        }
    }

    async fn shutdown(&self) {
        let mut child = self.child.lock().await;
        if let Some(mut c) = child.take() {
            let _ = c.start_kill();
            let _ = c.wait().await;
        }
    }
}

/// Send a JSON value as one Content-Length-framed JSON-RPC message.

View on GitHub (pinned to 8880682c63)

Solutions

  1. Check whether the language server process is still alive; restart it (or reopen the session so the client respawns it)
  2. Run the configured server command manually with the same arguments to see its crash output
  3. Update the server version if the crash reproduces on specific files
  4. Raise memory limits if the server is being OOM-killed

Example fix

// before: request into a dead server
let reply = client.request("textDocument/definition", params).await; // Err(channel closed)
// after: detect exit, respawn once, then retry
if reply.is_err() && client.server_exited().await {
    client = LspClient::spawn(server_cmd).await?;
    reply = client.request("textDocument/definition", params).await;
}
Defensive patterns

Strategy: retry

Validate before calling

// Health gate before issuing requests: has the child exited?
fn server_alive(child: &mut tokio::process::Child) -> bool {
    child.try_wait().map(|status| status.is_none()).unwrap_or(false)
}

Try / catch

// One bounded restart, then surface the failure
for attempt in 0..2 {
    match client.request(method, params).await {
        Ok(reply) => break Ok(reply),
        Err(e) if e.to_string().contains("channel closed") && attempt == 0 => {
            client = restart_server().await?;
        }
        Err(e) => break Err(e),
    }
}

Prevention

When it happens

Trigger: Server binary crashes mid-request (panic, OOM kill), server exiting after stdin EOF, a wrong server command that starts and immediately stops, or client teardown dropping the reader task while requests are in flight.

Common situations: Flaky language servers on large workspaces, nightly server builds crashing on specific files, system OOM killers, containers reaping background processes.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/97141d6ff4a8ab21. Report an issue: GitHub.