leptos-rs/leptos · critical

At {caller}, tried to spawn a Future with Executor::spawn_lo

Error message

At {caller}, tried to spawn a Future with Executor::spawn_local() before a global executor was initialized.

What it means

Debug-build panic raised by handle_uninitialized_spawn_local when Executor::spawn_local is called before a global spawn_local executor is registered. It includes the {caller} location to identify the offending call site. It is the debug counterpart of the release-mode no_op_spawn_local panic.

Source

Thrown at any_spawner/src/lib.rs:504

/// Handles the case where `Executor::spawn_local` is called without an initialized executor.
#[cold] // Less likely path
#[inline(never)]
#[track_caller]
fn handle_uninitialized_spawn_local(_fut: PinnedLocalFuture<()>) {
    let caller = std::panic::Location::caller();
    #[cfg(all(debug_assertions, feature = "tracing"))]
    {
        tracing::error!(
            target: "any_spawner",
            spawn_caller=%caller,
            "Executor::spawn_local called before a global executor was initialized. \
            Task likely dropped or panicked."
        );
        // Fall through to panic or no-op depending on build/target
    }
    #[cfg(all(debug_assertions, not(feature = "tracing")))]
    {
        panic!(
            "At {caller}, tried to spawn a Future with \
             Executor::spawn_local() before a global executor was initialized."
        );
    }
    // In release builds (without tracing), call the specific no-op function (which usually panics).
    #[cfg(not(debug_assertions))]
    {
        no_op_spawn_local(_fut);
    }
}

View on GitHub (pinned to 32d20f6c9d)

Solutions

  1. Register the local executor via any_spawner::spawn_local::set_executor(...) before the first spawn_local call.
  2. Ensure the registration happens on the same thread the futures are spawned on (LocalSet semantics).
  3. Use the {caller} in the message to locate the premature spawn_local and move it after initialization.
  4. If the message is replaced by a no-op panic in release, reproduce in debug to get the caller info, then fix init order.

Example fix

// before
fn handler() {
    Executor::spawn_local(async { update_dom().await }); // panics in debug
}
// after
fn handler() {
    ensure_executor_installed(); // any_spawner::spawn_local::set_executor(...)
    Executor::spawn_local(async { update_dom().await });
}
Defensive patterns

Strategy: try-catch

Validate before calling

fn spawn_local_task<F: Future<Output = ()> + 'static>(fut: F) {
    crate::init::ensure_local_executor_installed();
    any_spawner::spawn_local(fut);
}

Try / catch

std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
    any_spawner::spawn_local(async {});
})).err().map(|p| eprintln!("spawn_local before executor init: {:?}", p));

Prevention

When it happens

Trigger: Executor::spawn_local invoked with no configured global spawn_local function, in a debug build without the tracing feature.

Common situations: Tests or early startup code using spawn_local before executor setup; wasm code paths where only the non-local executor was registered; switching frameworks/feature flags and losing the LocalSet registration.

Related errors


AI-assisted analysis of leptos-rs/leptos@32d20f6c9d (2026-09-01). Data as JSON: /api/errors/4a7ada64ad183835. Report an issue: GitHub.