{"record":{"id":"b719735af047772e","repo":"gitbutlerapp/gitbutler","slug":"failed-to-create-tokio-runtime","errorCode":null,"errorMessage":"failed to create tokio runtime","messagePattern":"failed to create tokio runtime","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/gitbutler-user/src/api.rs","lineNumber":258,"sourceCode":"        }\n        resp.json()\n            .await\n            .context(\"Failed to parse profile update response\")\n    })\n}\n\n/// Execute an async future on a dedicated thread with its own Tokio runtime.\n///\n/// This keeps the crate's public API synchronous while still using async HTTP\n/// internally, following the same pattern as `but-forge`.\nfn run_async<F, T>(future: F) -> Result<T>\nwhere\n    F: std::future::Future<Output = Result<T>> + Send + 'static,\n    T: Send + 'static,\n{\n    std::thread::spawn(move || {\n        tokio::runtime::Runtime::new()\n            .expect(\"failed to create tokio runtime\")\n            .block_on(future)\n    })\n    .join()\n    .map_err(|e| anyhow::anyhow!(\"thread panicked: {e:?}\"))?\n}\n\n#[cfg(test)]\nmod tests {\n    use super::api_url_override_from_env;\n\n    #[test]\n    fn prefers_backend_specific_override() {\n        let url = api_url_override_from_env(|key| match key {\n            \"GITBUTLER_API_URL\" => Some(\"https://backend.example.com\".to_string()),\n            \"PUBLIC_API_BASE_URL\" => Some(\"https://frontend.example.com\".to_string()),\n            _ => None,\n        });\n","sourceCodeStart":240,"sourceCodeEnd":276,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/gitbutler-user/src/api.rs#L240-L276","documentation":"`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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check fd usage (lsof -p <pid> | wc -l) and raise limits (ulimit -n, LimitNOFILE) if the cause is EMFILE","Share one lazily-created runtime across calls instead of one per call to cut resource churn","Propagate instead of expect: build the runtime with .context(\"failed to create tokio runtime\")? inside the thread and return Result","In constrained environments use a single-threaded runtime (new_current_thread) or trim the driver set"],"exampleFix":"// before\nstd::thread::spawn(move || {\n    tokio::runtime::Runtime::new()\n        .expect(\"failed to create tokio runtime\")\n        .block_on(future)\n})\n.join()\n.map_err(|e| anyhow::anyhow!(\"thread panicked: {e:?}\"))\n\n// after\nstd::thread::spawn(move || -> Result<T> {\n    tokio::runtime::Runtime::new()\n        .context(\"failed to create tokio runtime\")?\n        .block_on(future)\n})\n.join()\n.map_err(|e| anyhow::anyhow!(\"thread panicked: {e:?}\"))","handlingStrategy":"try-catch","validationCode":"// cheap pre-flight: can the process still allocate a file descriptor?\nfn fds_available() -> bool {\n    std::fs::File::open(\"/dev/null\").is_ok()\n}","typeGuard":null,"tryCatchPattern":"match tokio::runtime::Runtime::new() {\n    Ok(rt) => rt.block_on(future),\n    Err(e) => Err(anyhow::Error::new(e).context(\"failed to create tokio runtime\")),\n}","preventionTips":["Reuse a global runtime for one-off async bridges","Monitor open-file counts in long-lived processes","Set a sane RLIMIT_NOFILE in packaged apps and service definitions"],"tags":["rust","tokio","runtime","thread","resource-exhaustion"],"backgroundTag":"async-runtime-init-failed","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}