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
- Reproduce with the same input and get the panic message backtrace (set RUST_BACKTRACE=1) to find the real crash site
- Fix the panicking code path in process_stream/highlighter to return Result instead of unwrapping
- Check the input data for edge cases (empty input, invalid UTF-8, extremely large lines) that trigger the panic
- 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
- Avoid unwrap/expect/indexing in stream-processing code; return Result instead
- Fuzz or test process_stream with edge-case inputs (empty, invalid UTF-8, huge lines)
- Keep the panic-to-event catch_unwind safety net but also log the original panic payload
- Set panic = "abort" only consciously; for this design keep unwind enabled so the fallback error can be produced
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)