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

  1. Audit the worker loop so every exit path sends a `TravelWorkerEvent::Complete(result)` before dropping the sender.
  2. Check `worker.join()` result and surface the worker's actual panic/error instead of the generic message.
  3. 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

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


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)