leptos-rs/leptos · error
lock poisoned
Error message
lock poisoned
What it means
ReactiveFunction::invoke for Arc<Mutex<dyn FnMut() -> T>> locks the mutex and panics if it is poisoned. Poisoning happens when another thread panicked while holding the lock, leaving the wrapped closure in an inconsistent state; the library chooses to propagate that as a panic rather than silently skip the function.
Source
Thrown at tachys/src/reactive_graph/mod.rs:594
pub type SharedReactiveFunction<T> = Arc<Mutex<dyn FnMut() -> T + Send>>;
/// A reactive view function.
pub trait ReactiveFunction: Send + 'static {
/// The return type of the function.
type Output;
/// Call the function.
fn invoke(&mut self) -> Self::Output;
/// Converts the function into a cloneable, shared type.
fn into_shared(self) -> Arc<Mutex<dyn FnMut() -> Self::Output + Send>>;
}
impl<T: 'static> ReactiveFunction for Arc<Mutex<dyn FnMut() -> T + Send>> {
type Output = T;
fn invoke(&mut self) -> Self::Output {
let mut fun = self.lock().expect("lock poisoned");
fun()
}
fn into_shared(self) -> Arc<Mutex<dyn FnMut() -> Self::Output + Send>> {
self
}
}
impl<T: Send + Sync + 'static> ReactiveFunction
for Arc<dyn Fn() -> T + Send + Sync>
{
type Output = T;
fn invoke(&mut self) -> Self::Output {
self()
}
fn into_shared(self) -> Arc<Mutex<dyn FnMut() -> Self::Output + Send>> {View on GitHub (pinned to 32d20f6c9d)
Solutions
- Fix the underlying panic inside the FnMut closure so the mutex is never poisoned
- Wrap the closure body so transient errors return a default T instead of panicking
- If a panic is unavoidable/recoverable, catch it at the outer reactive boundary and rebuild the function value
- Avoid calling .expect/unwraps inside reactive closures on possibly-invalid data
Example fix
// before let f = Arc::new(Mutex::new(move || other.signal.get_untracked().unwrap())); // panics -> poison // after let f = Arc::new(Mutex::new(move || other.signal.get_untracked().unwrap_or_default()));
Defensive patterns
Strategy: try-catch
Try / catch
// Mutex poisoning surfaces at lock(); guard the closure body so it never panics,
// and catch panics at the reactive boundary:
let out = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
fun_clone.get_untracked()
}));
if out.is_err() { log::error("reactive function panicked; mutex may be poisoned"); } Prevention
- Never unwrap/expect/index-panic inside reactive closures
- Return Default/default values for invalid inputs in FnMut closures
- Catch panics at task boundaries before they poison shared locks
- Consider parking_lot::Mutex (poison-free) for shared reactive state in your own code
When it happens
Trigger: A closure passed as a reactive function (e.g. as a signal/derived dependency) panicked on some thread while holding the lock; subsequently any invoke() call hits the poisoned mutex.
Common situations: A derived/computation closure that indexes out of bounds or unwraps None on one render pass, then the reactive graph re-invokes it later; panics inside event handlers guarded by the same Arc<Mutex<...>>.
Related errors
- no reactive owner
- Executor::spawn called, but no global 'spawn' function is co
- Executor::spawn_local called, but no global 'spawn_local' fu
- At {caller}, tried to spawn a Future with Executor::spawn()
- At {caller}, tried to spawn a Future with Executor::spawn_lo
AI-assisted analysis of leptos-rs/leptos@32d20f6c9d (2026-09-01).
Data as JSON: /api/errors/57792f86679bd96e.
Report an issue: GitHub.