gitbutlerapp/gitbutler · critical
thread panicked
Error message
thread panicked: {e:?} What it means
run_async() in gitbutler-user spawns a dedicated OS thread running a fresh tokio runtime to block on an async future. If that thread panics (JoinHandle::join returns Err), the panic payload is reformatted into this anyhow error. It wraps any panic inside the async user-API futures (login token, user fetch/profile, upload).
Solutions
- Inspect the attached panic payload ({e:?}) to find the panicking callsite and fix the underlying cause.
- Check the inner unwrap/expect in the user API path that the payload points to.
- Retry the operation after ruling out transient data issues.
- Report a bug with the panic payload if it comes from library code.
Example fix
// before
let user = fetch_user_by_token(token).unwrap(); // panic propagates as "thread panicked"
// after
match fetch_user_by_token(token) {
Ok(user) => /* ... */,
Err(e) => log::error!("user fetch failed: {e:#}"),
} Defensive patterns
Strategy: try-catch
Try / catch
match run_async(fetch_user_by_token(token)) {
Ok(v) => v,
Err(e) if e.to_string().starts_with("thread panicked") => {
log::error!("panic in user API: {e:#}");
Err(e)
}
Err(e) => Err(e),
} Prevention
- Avoid unwrap/expect inside async user-API futures; return Results instead.
- Keep panic payloads in logs for diagnosis.
- Fix underlying causes indicated by the panic payload rather than retrying blindly.
When it happens
Trigger: A panic anywhere inside fetch_login_token, fetch_user_by_token, fetch_user_profile, update_user_profile, or upload_file futures — e.g. explicit panic!/unwrap/expect on bad data, or runtime creation failing in the spawned thread.
Common situations: Malformed API responses hitting an unwrap; poisoned state in the future; out-of-memory when constructing the tokio runtime; bugs surfaced during token refresh.
Related errors
- Archive progress counter thread panicked
- Failed to join thread
- Failed to join thread
- GenericFailure
- handler task panicked
AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18).
Data as JSON: /api/errors/cded2df0a8758ea4.
Report an issue: GitHub.
Appendix: source
Thrown at crates/gitbutler-user/src/api.rs:337
})
}
/// Execute an async future on a dedicated thread with its own Tokio runtime.
///
/// This keeps the crate's public API synchronous while still using async HTTP
/// internally, following the same pattern as `but-forge`.
fn run_async<F, T>(future: F) -> Result<T>
where
F: std::future::Future<Output = Result<T>> + Send + 'static,
T: Send + 'static,
{
std::thread::spawn(move || {
tokio::runtime::Runtime::new()
.expect("failed to create tokio runtime")
.block_on(future)
})
.join()
.map_err(|e| anyhow::anyhow!("thread panicked: {e:?}"))?
}
#[cfg(test)]
mod tests {
use super::api_url_override_from_env;
#[test]
fn prefers_backend_specific_override() {
let url = api_url_override_from_env(|key| match key {
"GITBUTLER_API_URL" => Some("https://backend.example.com".to_string()),
"PUBLIC_API_BASE_URL" => Some("https://frontend.example.com".to_string()),
_ => None,
});
assert_eq!(url.as_deref(), Some("https://backend.example.com"));
}
#[test]View on GitHub (pinned to 58e5313667)