gitbutlerapp/gitbutler · error

failed to create tokio runtime

Error message

failed to create tokio runtime

What it means

`tokio::runtime::Runtime::new()` creates a multi-thread runtime with I/O and time drivers; it fails when the OS refuses resources the drivers need (epoll or timerfd creation under fd exhaustion, memory pressure) or in restricted sandboxes. run_async() executes the future on a dedicated thread with its own runtime; the inner `.expect` panics only that thread, and `.join()` converts the panic into an anyhow error ("thread panicked"), so callers see an Err rather than a process crash.

Source

Thrown at crates/gitbutler-user/src/api.rs:258

        }
        resp.json()
            .await
            .context("Failed to parse profile update response")
    })
}

/// 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,
        });

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Check fd usage (lsof -p <pid> | wc -l) and raise limits (ulimit -n, LimitNOFILE) if the cause is EMFILE
  2. Share one lazily-created runtime across calls instead of one per call to cut resource churn
  3. Propagate instead of expect: build the runtime with .context("failed to create tokio runtime")? inside the thread and return Result
  4. In constrained environments use a single-threaded runtime (new_current_thread) or trim the driver set

Example fix

// before
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:?}"))

// after
std::thread::spawn(move || -> Result<T> {
    tokio::runtime::Runtime::new()
        .context("failed to create tokio runtime")?
        .block_on(future)
})
.join()
.map_err(|e| anyhow::anyhow!("thread panicked: {e:?}"))
Defensive patterns

Strategy: try-catch

Validate before calling

// cheap pre-flight: can the process still allocate a file descriptor?
fn fds_available() -> bool {
    std::fs::File::open("/dev/null").is_ok()
}

Try / catch

match tokio::runtime::Runtime::new() {
    Ok(rt) => rt.block_on(future),
    Err(e) => Err(anyhow::Error::new(e).context("failed to create tokio runtime")),
}

Prevention

When it happens

Trigger: Any sync call into crates/gitbutler-user/src/api.rs reaching run_async() while the process is out of file descriptors (EMFILE) or memory: Runtime::new() inside the spawned thread fails, the thread panics, and join() maps the Box<dyn Any> payload into an anyhow error.

Common situations: A long-running desktop app with an fd leak from watchers or sockets; containers or launchd services with low RLIMIT_NOFILE; bursts of concurrent API calls each spawning a fresh thread plus runtime.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/b719735af047772e. Report an issue: GitHub.