Hmbown/CodeWhale · error

lifecycle outbox writer task is gone

Error message

lifecycle outbox writer task is gone

What it means

enqueue's bounded channel send returned TrySendError::Closed, meaning the spawned lifecycle outbox writer task no longer exists (it panicked, was aborted, or the receiver was dropped). The library surfaces this instead of silently losing events.

Solutions

  1. Restart the outbox/writer (ensure_writer_spawned re-arms it; recreate the outbox instance if it stays closed)
  2. Find and fix the panic in the writer task's flush loop from the task's logs
  3. Avoid emitting events after runtime shutdown; gate emitters on shutdown signals
Defensive patterns

Strategy: fallback

Validate before calling

if outbox.writer_closed() {
    eprintln!("outbox writer gone; re-spawning before emitting");
    outbox.ensure_writer_spawned();
}

Try / catch

match outbox.enqueue(event) {
    Err(e) if e.to_string().contains("writer task is gone") => {
        outbox.ensure_writer_spawned();
        outbox.enqueue(event).or_else(|_| { dead_letter_log(event); Ok(()) })?;
        Ok(())
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling enqueue after the writer task died — e.g. runtime shutdown, a panic in the writer's flush loop, or explicit abort of the task.

Common situations: Application shutting down while events are still emitted; a bug or I/O failure panicking the writer task; runtime teardown ordering issues.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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

Appendix: source

Thrown at crates/hooks/src/lifecycle_outbox.rs:245

    /// is bounded: a wedged consumer drops further events with a warning
    /// (observability, not control flow) but fails a flush fast instead of
    /// dropping its reply channel.
    fn enqueue(self: &Arc<Self>, command: OutboxCommand) -> Result<()> {
        let is_flush = matches!(command, OutboxCommand::Flush(_));
        match self.sender.try_send(command) {
            Ok(()) => {}
            Err(TrySendError::Full(_)) if !is_flush => {
                tracing::warn!(
                    target: "lifecycle_outbox",
                    queue_capacity = OUTBOX_QUEUE_CAPACITY,
                    "lifecycle event dropped: outbox queue is full"
                );
            }
            Err(TrySendError::Full(_)) => {
                anyhow::bail!("lifecycle outbox queue is full; flush rejected");
            }
            Err(TrySendError::Closed(_)) => {
                anyhow::bail!("lifecycle outbox writer task is gone");
            }
        }
        self.ensure_writer_spawned();
        Ok(())
    }

    fn ensure_writer_spawned(self: &Arc<Self>) {
        if self.writer_spawned.load(Ordering::Acquire) {
            return;
        }
        let _guard = self
            .spawn_lock
            .lock()
            .unwrap_or_else(|poisoned| poisoned.into_inner());
        if self.writer_spawned.load(Ordering::Acquire) {
            return;
        }
        let Ok(handle) = tokio::runtime::Handle::try_current() else {

View on GitHub (pinned to 73e0f67d83)