zellij-org/zellij · error · anyhow::Error
failed to send message to channel
Error message
failed to send message to channel
What it means
Production (non-debug) variant of the ToAnyhow implementation for SendError in zellij-utils/errors.rs: a message sent over an internal channel could not be delivered because the receiver was dropped. The ErrorContext is still attached so the error chain shows which subsystem dispatched the send, but the unsent payload is omitted for a smaller message. Fired whenever DEBUG_MODE is false/unset and .to_anyhow() is applied to a failed send.
Source
Thrown at zellij-utils/src/errors.rs:962
///
/// Takes the `SendError` and creates an `anyhow` error type with the message that was sent
/// (formatted as string), attaching the [`ErrorContext`] as anyhow context to it.
impl<T: std::fmt::Debug, U> ToAnyhow<U>
for Result<U, crate::channels::SendError<(T, ErrorContext)>>
{
fn to_anyhow(self) -> anyhow::Result<U> {
match self {
Ok(val) => anyhow::Ok(val),
Err(e) => {
let (msg, context) = e.into_inner();
if *crate::consts::DEBUG_MODE.get().unwrap_or(&true) {
Err(anyhow::anyhow!(
"failed to send message to channel: {:#?}",
msg
))
.with_context(|| context.to_string())
} else {
Err(anyhow::anyhow!("failed to send message to channel"))
.with_context(|| context.to_string())
}
},
}
}
}
impl<U> ToAnyhow<U> for Result<U, std::sync::PoisonError<U>> {
fn to_anyhow(self) -> anyhow::Result<U> {
match self {
Ok(val) => anyhow::Ok(val),
Err(e) => {
if *crate::consts::DEBUG_MODE.get().unwrap_or(&true) {
Err(anyhow::anyhow!("cannot acquire poisoned lock for {e:#?}"))
} else {
Err(anyhow::anyhow!("cannot acquire poisoned lock"))
}
},View on GitHub (pinned to 98a0837077)
Solutions
- Search the log above this error for the original panic or shutdown of the receiver thread and address that root cause
- Restart the session to restore consistent channel state
- Reproduce with `zellij --debug` (or set debug mode) to get the debug variant that names the unsent message
- Update zellij to pick up concurrency fixes; report persistent reproduction
Example fix
// before
bus.senders.send_to_screen.send((screen_msg, ErrorContext::new())).to_anyhow()?;
// after: treat a dead receiver as termination, not a fatal error
if let Err(e) = bus.senders.send_to_screen.send((screen_msg, ErrorContext::new())).to_anyhow() {
log::warn!("screen channel closed, stopping emitter: {e:#}");
break;
} Defensive patterns
Strategy: try-catch
Validate before calling
if !sender.is_connected_to_any_receiver() {
log::debug!("receiver dropped; not sending");
return Ok(());
} Try / catch
match sender.send((msg, ErrorContext::new())).to_anyhow() {
Ok(_) => {}
Err(e) => {
log::warn!("channel closed, stopping emitter: {e:#}");
break; // graceful: unwind this producer instead of failing the caller
},
} Prevention
- Mirror shutdown signals (crossbeam Select/threads) so producers exit before consumers drop
- Run with debug mode when diagnosing to get the payload-bearing variant of this error
- Log receiver-thread lifecycle (start/exit/panic) to correlate with send failures
- Avoid sending from timers/plugins after initiating session quit
When it happens
Trigger: The same send-after-receiver-death race as the debug variant: worker thread owning the receiver has exited (panic, shutdown, disconnect) while another thread calls send(...).to_anyhow()? on the shared channel.
Common situations: Session teardown racing pending input; earlier panic in screen/router thread; abrupt client disconnect; long-running automation scripting the CLI against a server that is closing.
Related errors
- failed to send message to channel: {:#?}
- Failed to convert to protobuf: {:?}
- found no sender to send plugin instruction to
- No tabs left, cannot move clients: {:?} from closed tab
- cannot acquire poisoned lock for {e:#?}
AI-assisted analysis of zellij-org/zellij@98a0837077 (2026-08-16).
Data as JSON: /api/errors/b82fc79589b0bcbb.
Report an issue: GitHub.