bensadeh/tailspin · critical · anyhow::Error

stream thread panicked

Error message

stream thread panicked

What it means

This error is a synthesized fallback: when the streaming worker thread panics while processing the reader output, catch_unwind captures the panic and the code substitutes this anyhow error instead of letting the recv loop block forever. It means the stream-processing logic crashed unexpectedly, not that the underlying data was bad.

Solutions

  1. Reproduce with the same input and get the panic message backtrace (set RUST_BACKTRACE=1) to find the real crash site
  2. Fix the panicking code path in process_stream/highlighter to return Result instead of unwrapping
  3. Check the input data for edge cases (empty input, invalid UTF-8, extremely large lines) that trigger the panic
  4. File a bug with the input and backtrace if the panic is inside the library code

Example fix

// before
let first = batch.lines.first().unwrap();
// after
let Some(first) = batch.lines.first() else { return Ok(()); };
Defensive patterns

Strategy: try-catch

Validate before calling

RUST_BACKTRACE=1 myapp --exec "..." 2>panic.log  # capture the real panic location first

Try / catch

match result {
    Err(e) if e.to_string() == "stream thread panicked" => {
        eprintln!("stream crashed; see backtrace with RUST_BACKTRACE=1");
        // fall back to rendering without the streaming path
    }
    Err(e) => return Err(e),
    Ok(_) => {}
}

Prevention

When it happens

Trigger: A panic (unwrap on None, index out of bounds, assertion, explicit panic) inside process_stream executed on the spawned thread; catch_unwind converts the unwind into Err(anyhow!("stream thread panicked")).

Common situations: Highlighter bugs on unusual input (e.g. malformed ANSI or very long lines); a regression in process_stream that unwraps on an empty batch; memory-pressure aborts surfacing as panics in dependent code paths.


AI-assisted analysis of bensadeh/tailspin@8ecaa9a8a1 (2026-09-13). Data as JSON: /api/errors/5c675cafef8a580c. Report an issue: GitHub.

Appendix: source

Thrown at src/main.rs:65

    let (initial_read_tx, _) = mpsc::channel();

    BrokenPipe::suppress(process_stream(reader, writer, highlighter, initial_read_tx))
}

/// Runs the stream on its own thread while the pager runs as a child process;
/// whichever finishes first decides what happens to the other.
fn run_with_pager(reader: Reader, writer: Writer, highlighter: Highlighter, pager: Pager) -> anyhow::Result<()> {
    let exec_child = reader.exec_child();
    let (initial_read_tx, initial_read_rx) = mpsc::channel();
    let (events_tx, events) = mpsc::channel();

    let stream_tx = events_tx.clone();
    thread::spawn(move || {
        // A panic must still produce an event, or the recv loop blocks forever
        let result = catch_unwind(AssertUnwindSafe(|| {
            process_stream(reader, writer, &highlighter, initial_read_tx)
        }))
        .unwrap_or_else(|_| Err(anyhow::anyhow!("stream thread panicked")));
        let _ = stream_tx.send(Event::Stream(result));
    });

    if initial_read_rx.recv().is_err() {
        let Event::Stream(result) = events.recv()? else {
            unreachable!("the pager is not spawned yet")
        };
        return result;
    }

    let pager_child = match pager.spawn() {
        Ok(pager_child) => pager_child,
        Err(e) => {
            kill_exec_child(exec_child.as_deref());
            return Err(e);
        }
    };
    let waiter = pager_child.waiter();

View on GitHub (pinned to 8ecaa9a8a1)