leptos-rs/leptos · critical
failed to spawn future on ThreadPool
Error message
failed to spawn future on ThreadPool
What it means
Panics when the futures-executor `ThreadPool::spawn(fut)` call inside the globally installed ExecutorFns returns Err. `ThreadPool::spawn` fails when the pool has been closed or when the pool object no longer has any threads registered (e.g. its threads were unable to start or all exited), so the future cannot be queued. any_spawner's spawn closure `.expect()`s on it, turning a spawn failure into an immediate panic in the caller's thread.
Source
Thrown at any_spawner/src/lib.rs:274
static THREAD_POOL: OnceLock<ThreadPool> = OnceLock::new();
thread_local! {
static LOCAL_POOL: RefCell<LocalPool> = RefCell::new(LocalPool::new());
// SPAWNER is derived from LOCAL_POOL, keep it for efficiency inside the closure
static SPAWNER: LocalSpawner = LOCAL_POOL.with(|pool| pool.borrow().spawner());
}
fn get_thread_pool() -> &'static ThreadPool {
THREAD_POOL.get_or_init(|| {
ThreadPool::new()
.expect("could not create futures executor ThreadPool")
})
}
let executor_impl = ExecutorFns {
spawn: |fut| {
get_thread_pool()
.spawn(fut)
.expect("failed to spawn future on ThreadPool");
},
spawn_local: |fut| {
// Use the thread_local SPAWNER derived from LOCAL_POOL
SPAWNER.with(|spawner| {
spawner
.spawn_local(fut)
.expect("failed to spawn local future");
});
},
poll_local: || {
// Use the thread_local LOCAL_POOL
LOCAL_POOL.with(|pool| {
// Use try_borrow_mut to prevent panic during re-entrant calls
if let Ok(mut pool) = pool.try_borrow_mut() {
pool.run_until_stalled();
}
// If already borrowed, we're likely in a nested poll, so do nothing.
});View on GitHub (pinned to 32d20f6c9d)
Solutions
- Prefer initializing with a runtime-backed executor (`init_tokio`, `init_async_executor`) so spawning is tied to a live runtime instead of a standalone ThreadPool
- Ensure the executor is initialized once at program start before any spawning, and keep the process alive as long as tasks may be spawned
- Audit worker-thread panics (panic hooks) that could close/empty the pool; fix or guard those task bodies
- Avoid spawning tasks during process shutdown; gate spawns behind an app-lifecycle flag
Example fix
// before any_spawner::executor::init_futures_executor().unwrap(); // after // bind spawning to the tokio runtime the server already runs on any_spawner::executor::init_tokio().unwrap();
Defensive patterns
Strategy: try-catch
Try / catch
// spawning through any_spawner panics on failure; keep task-spawning entry points
// isolated and catch unwind at the boundary
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
any_spawner::executor::spawn(async { /* task */ });
}));
if result.is_err() {
eprintln!("spawn failed: thread pool unavailable");
} Prevention
- Use init_tokio/init_async_executor instead of the standalone futures ThreadPool when possible
- Install a panic hook to spot worker-thread panics that can empty the pool
- Don't spawn tasks after shutdown has begun
- Test startup on constrained hosts (low pids limit) in CI
When it happens
Trigger: After `init_futures_executor()`, calling any API that routes through `any_spawner::executor::spawn(...)` (e.g. Leptos tasks, `spawn` from a reactive scope) when the lazily created ThreadPool was closed or its worker threads never registered/failed.
Common situations: ThreadPool thread panics or the process is shutting down while tasks are still being spawned; exotic hosts where the pool's threads failed to register; mixing executor features so tasks spawn after pool teardown; running under environments that kill worker threads (signal handlers, fork after threads created).
Related errors
- Unsupported server function HTTP method: {method:?}
- {}
- Reading from a LocalResource outside Suspense in `ssr` mode
- at {location}, the `sandboxed-arenas` feature is active, but
- could not create futures executor ThreadPool
AI-assisted analysis of leptos-rs/leptos@32d20f6c9d (2026-09-01).
Data as JSON: /api/errors/53cd143de134cf41.
Report an issue: GitHub.