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

  1. Fix the underlying panic inside the FnMut closure so the mutex is never poisoned
  2. Wrap the closure body so transient errors return a default T instead of panicking
  3. If a panic is unavoidable/recoverable, catch it at the outer reactive boundary and rebuild the function value
  4. 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

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


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