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

  1. Search the log above this error for the original panic or shutdown of the receiver thread and address that root cause
  2. Restart the session to restore consistent channel state
  3. Reproduce with `zellij --debug` (or set debug mode) to get the debug variant that names the unsent message
  4. 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

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


AI-assisted analysis of zellij-org/zellij@98a0837077 (2026-08-16). Data as JSON: /api/errors/b82fc79589b0bcbb. Report an issue: GitHub.