GitoxideLabs/gitoxide · error
time-travel worker stopped unexpectedly
Error message
time-travel worker stopped unexpectedly
What it means
The UI event loop receives events from a time-travel worker thread over an mpsc channel. When `recv_timeout` returns `RecvTimeoutError::Disconnected`, the worker's sending half was dropped — meaning the background worker terminated without sending a `Complete` result. The loop breaks with this error.
Solutions
- Audit the worker loop so every exit path sends a `TravelWorkerEvent::Complete(result)` before dropping the sender.
- Check `worker.join()` result and surface the worker's actual panic/error instead of the generic message.
- Reproduce the premature worker exit with tracing enabled to find the early-return path.
Example fix
// before
Err(mpsc::RecvTimeoutError::Disconnected) => {
break Err(anyhow::anyhow!("time-travel worker stopped unexpectedly"));
}
// after
Err(mpsc::RecvTimeoutError::Disconnected) => {
let worker_err = worker.join().err().map(|_| "worker panicked");
break Err(anyhow::anyhow!("time-travel worker stopped unexpectedly: {worker_err:?}"));
} Defensive patterns
Strategy: try-catch
Validate before calling
if worker.is_finished() {
eprintln!("time-travel worker already exited; skipping event wait");
return;
} Type guard
fn worker_alive(worker: &std::thread::JoinHandle<()>) -> bool { !worker.is_finished() } Try / catch
match receiver.recv_timeout(dur) {
Ok(ev) => handle(ev),
Err(mpsc::RecvTimeoutError::Timeout) => {},
Err(mpsc::RecvTimeoutError::Disconnected) => {
let cause = worker.join().err();
break Err(anyhow!("time-travel worker stopped: {cause:?}"));
}
} Prevention
- Ensure worker threads always send a terminal event before returning.
- Wrap worker bodies so panics still deliver an error event over the channel.
- Keep the channel sender alive until the terminal event is sent.
When it happens
Trigger: `receiver.recv_timeout(...)` returns `Disconnected` because the time-travel worker thread dropped its `Sender` (worker finished its function early, returned without sending, or its channel sender was closed).
Common situations: Worker thread exiting early due to an error path that doesn't send a terminal event; worker thread killed by a panic whose JoinHandle isn't checked yet; premature drop of the worker sender during shutdown races.
Related errors
- history worker stopped unexpectedly
- time-travel worker panicked
- rebase worker stopped unexpectedly
- rebase worker panicked
AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08).
Data as JSON: /api/errors/a243804e6208d623.
Report an issue: GitHub.
Appendix: source
Thrown at gix-tix/src/lib.rs:5749
let _ = sender.send(TravelWorkerEvent::Complete(result));
});
let mut last_draw: Option<Instant> = None;
let mut latest = None;
let mut rendered = None;
let mut complete = None;
let mut render_error = None;
let result = loop {
let event = if complete.is_some() {
None
} else if render_error.is_none()
&& latest != rendered
&& let Some(last_draw) = last_draw
{
match receiver.recv_timeout(FRAME_INTERVAL.saturating_sub(last_draw.elapsed())) {
Ok(event) => Some(event),
Err(mpsc::RecvTimeoutError::Timeout) => None,
Err(mpsc::RecvTimeoutError::Disconnected) => {
break Err(anyhow::anyhow!("time-travel worker stopped unexpectedly"));
}
}
} else {
match receiver.recv() {
Ok(event) => Some(event),
Err(_) => break Err(anyhow::anyhow!("time-travel worker stopped unexpectedly")),
}
};
match event {
Some(TravelWorkerEvent::Rebased(id)) => latest = Some(id),
Some(TravelWorkerEvent::Complete(result)) => complete = Some(result),
None => {}
}
if render_error.is_none()
&& latest != rendered
&& (last_draw.is_none()
|| last_draw.is_some_and(|last_draw| last_draw.elapsed() >= FRAME_INTERVAL)
|| complete.is_some())View on GitHub (pinned to e73179060b)