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
- Check whether the language server process is still alive; restart it (or reopen the session so the client respawns it)
- Run the configured server command manually with the same arguments to see its crash output
- Update the server version if the crash reproduces on specific files
- 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
- Watch the server child process; mark the client dead on exit so requests fail fast
- Log server stderr to a file for post-crash diagnosis
- Pin known-good language server versions
- Bound restarts to one attempt to avoid restart loops
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
- LSP outbound channel closed
- dsh exited with status {code}
- Failed to run command: {e}
- stdout unavailable
- stderr unavailable
AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16).
Data as JSON: /api/errors/97141d6ff4a8ab21.
Report an issue: GitHub.