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

  1. Inspect the attached panic payload ({e:?}) to find the panicking callsite and fix the underlying cause.
  2. Check the inner unwrap/expect in the user API path that the payload points to.
  3. Retry the operation after ruling out transient data issues.
  4. 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

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


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)