leptos-rs/leptos · critical

Tried to access a reactive value that has already been dispo

Error message

Tried to access a reactive value that has already been disposed.

What it means

Accessing a disposed reactive value (signal/memo) in debug builds panics with the definition and access locations. When a reactive owner is disposed (component unmounted or owner dropped), its signals' storage is freed; reading them afterwards is a use-after-free bug in the reactive graph. This is the debug-assertions branch of the disposed-signal panic macro, which includes source locations in the message.

Source

Thrown at reactive_graph/src/traits.rs:75

};
use any_spawner::Executor;
use futures::{Stream, StreamExt};
use std::{
    ops::{Deref, DerefMut},
    panic::Location,
};

#[doc(hidden)]
/// Provides a sensible panic message for accessing disposed signals.
#[macro_export]
macro_rules! unwrap_signal {
    ($signal:ident) => {{
        #[cfg(any(debug_assertions, leptos_debuginfo))]
        let location = std::panic::Location::caller();
        || {
            #[cfg(any(debug_assertions, leptos_debuginfo))]
            {
                panic!(
                    "{}",
                    $crate::traits::panic_getting_disposed_signal(
                        $signal.defined_at(),
                        location
                    )
                );
            }
            #[cfg(not(any(debug_assertions, leptos_debuginfo)))]
            {
                panic!(
                    "Tried to access a reactive value that has already been \
                     disposed."
                );
            }
        }
    }};
}

View on GitHub (pinned to 32d20f6c9d)

Solutions

  1. Keep the owning scope alive as long as the value is needed: move signal creation up to a longer-lived owner or use a global store.
  2. Cancel or guard async work: check ownership before reading (use Owner::with or a generation flag) so callbacks after dispose do nothing.
  3. Use spawn_local within the component scope and abort/drop pending tasks on cleanup (on_cleanup).
  4. For data that must outlive components, put it in a global reactive store or context rather than a local signal.

Example fix

// before
let (count, set_count) = create_signal(0);
setTimeout(move || set_count.set(1), 5000); // may fire after dispose

// after
let (count, set_count) = create_signal(0);
on_cleanup(move || handle.clear_timeout()); // or guard reads with ownership check
setTimeout(move || set_count.set(1), 5000);
Defensive patterns

Strategy: try-catch

Validate before calling

// before storing/reading: ensure the owner is alive
if scope_is_active() { set_count.set(new_value); }

Type guard

fn signal_alive<T>(s: &ReadSignal<T>) -> bool { !s.try_get().is_none() } // heuristic; prefer ownership checks

Try / catch

std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| signal.get())).unwrap_or(default_value)

Prevention

When it happens

Trigger: Reading a signal/memo through a closure created by the disposed-signal macro after its owner was disposed — e.g. an async task or timeout callback holding a ReadSignal whose component has unmounted, or an event listener on a node that outlives the signal's scope.

Common situations: setTimeout/setInterval or async fetch resolving after the component unmounted and then set_* / read the signal; storing signals in a global or long-lived struct instead of in the owning scope; manual ownership via Owner::new without keeping the scope alive; hydration/SSR teardown reading client signals.

Related errors


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