tauri-apps/tauri · error
runtime already initialized
Error message
runtime already initialized
What it means
async_runtime::set installs a global tokio runtime handle into a `OnceCell`-style static. `OnceCell::set` fails if a runtime was already stored, and the library panics with 'runtime already initialized' because two runtimes cannot coexist as the global default.
Solutions
- Call `set` exactly once, at process startup, before any Tauri async API is used.
- Guard with `set` semantics: only call set if `async_runtime::RUNTIME`/runtime not yet initialized, or route all set() calls through a once-initialized helper (std::sync::Once).
- If a default runtime was already lazily created, do not call set at all — use `async_runtime::handle()` to reuse it.
- In tests, share one runtime across the test binary or use a serial test harness instead of calling set per-test.
Example fix
// before
fn main() {
let rt = tokio::runtime::Runtime::new().unwrap();
tauri::async_runtime::set(rt.handle().clone());
init_app(); // also calls set() -> panic
}
// after
static INIT: std::sync::Once = std::sync::Once::new();
fn init_runtime(handle: tokio::runtime::Handle) {
INIT.call_once(|| tauri::async_runtime::set(handle));
} Defensive patterns
Strategy: validation
Validate before calling
use std::sync::Once;
static RT_INIT: Once = Once::new();
fn ensure_runtime(handle: tauri::async_runtime::RuntimeHandle) {
RT_INIT.call_once(|| tauri::async_runtime::set(handle));
} Prevention
- Call async_runtime::set exactly once, before any Tauri async API
- Never call set after handle()/spawn() which lazily create the default runtime
- Centralize runtime init in a Once or main() only
- In tests, share a single runtime instead of per-test set()
When it happens
Trigger: Calling `tauri::async_runtime::set(handle)` more than once, or calling it after `async_runtime::handle()`/any spawned async work lazily initialized the default runtime via `get_or_init(default_runtime)`.
Common situations: Embedding Tauri in an app that already built a tokio runtime and calls set() in multiple places (e.g. both main and a test helper); running tests in parallel where each test calls set(); initializing Tauri after other code already triggered the lazy default runtime.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- invalid permission identifier
- missing command scope for key
- Not a CheckMenuItem
- Not a MenuItem
- Not a PredefinedMenuItem
AI-assisted analysis of tauri-apps/tauri@460ec35447 (2026-09-18).
Data as JSON: /api/errors/a3710fb0ea677c0c.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tauri/src/async_runtime.rs:261
/// tauri::async_runtime::set(tokio::runtime::Handle::current());
///
/// // bootstrap the tauri app...
/// // tauri::Builder::default().run().unwrap();
/// }
///
/// async fn do_something() {}
/// ```
///
/// # Panics
///
/// Panics if the runtime is already set.
pub fn set(handle: TokioHandle) {
RUNTIME
.set(GlobalRuntime {
runtime: None,
handle: RuntimeHandle::Tokio(handle),
})
.unwrap_or_else(|_| panic!("runtime already initialized"))
}
/// Returns a handle of the async runtime.
pub fn handle() -> RuntimeHandle {
let runtime = RUNTIME.get_or_init(default_runtime);
runtime.handle()
}
#[track_caller]
/// Runs a future to completion on runtime.
pub fn block_on<F: Future>(task: F) -> F::Output {
let runtime = RUNTIME.get_or_init(default_runtime);
runtime.block_on(task)
}
#[track_caller]
/// Spawns a future onto the runtime.
pub fn spawn<F>(task: F) -> JoinHandle<F::Output>View on GitHub (pinned to 460ec35447)