pub fn scope_catch<T>(
f: impl FnOnce() -> T,
on_error: impl Fn(ErrorInfo) + 'static,
) -> (ScopeHandle, T)Expand description
Creates an ownership scope that catches panics from the effects it owns.
One buggy widget’s panicking effect otherwise unwinds out of the whole flush. A boundary
confines the blast radius: a panic from any effect owned by this scope — at any depth — is
delivered to on_error instead of propagating, and the rest of the application keeps running.
§What happens on a panic
The panicking effect is disposed before on_error runs: its cleanups run, the children it
owns are torn down, and it is unhooked from the graph. This is deliberate. An effect that
panicked has already had its dependency tracking cut short, and leaving it live would re-run
and re-panic on the next invalidation — or immediately, since a panic during dependency
verification re-queues the effect. Disposing it makes the failure terminal for that effect and
leaves the handler to decide what replaces it, which is the same shape as Solid’s
ErrorBoundary. The rest of the scope survives.
The nearest enclosing handler wins; boundaries nest. With no handler anywhere above the effect, the panic propagates exactly as it does today.
§Caveats
- This catches panics, which are bugs. Recoverable failures — a fetch that 404s, a parse that
fails — belong in the graph as
Resultvalues so that downstream nodes can react to them; an error boundary is not a substitute for that. - Under
panic = "abort"there is nothing to catch and the process aborts. The boundary compiles and is simply never invoked. - The handler runs on the reactor’s thread, inside the flush that ran the failing effect. It should tear down or replace the subtree, not do lengthy work.
- A panic raised by
fitself — the scope’s own body, run synchronously — is not caught. Only effects owned by the scope are covered. - Coverage follows ownership, so an effect created inside
unownedis outside every boundary above it and its panics propagate as usual. That is the same opt-outunownedalready applies to disposal; take anOwnerandrun_into keep async work under the boundary that created it.
§Examples
use std::cell::RefCell;
use std::rc::Rc;
use adaptite::{Reactor, scope_catch, signal_in};
let reactor = Reactor::new();
let errors = Rc::new(RefCell::new(Vec::new()));
let healthy_runs = Rc::new(RefCell::new(0));
let value = signal_in(&reactor, 1);
let (boundary, ()) = scope_catch(
{
let reactor = reactor.clone();
let value = value.clone();
let healthy_runs = Rc::clone(&healthy_runs);
move || {
// A buggy widget...
reactor.effect({
let value = value.clone();
move || {
if value.get() == 2 {
panic!("widget exploded");
}
}
}).leak();
// ...and a healthy sibling that must survive it.
reactor.effect({
let value = value.clone();
let healthy_runs = Rc::clone(&healthy_runs);
move || {
value.get();
*healthy_runs.borrow_mut() += 1;
}
}).leak();
}
},
{
let errors = Rc::clone(&errors);
move |error: adaptite::ErrorInfo| {
errors.borrow_mut().push(error.message().unwrap_or("?").to_string());
}
},
);
reactor.flush_now();
assert!(errors.borrow().is_empty());
assert_eq!(*healthy_runs.borrow(), 1);
// The panic is contained: reported, not propagated.
value.set(2);
reactor.flush_now();
assert_eq!(*errors.borrow(), ["widget exploded"]);
assert_eq!(*healthy_runs.borrow(), 2, "the sibling still ran");
// And the application keeps working afterwards.
value.set(3);
reactor.flush_now();
assert_eq!(*healthy_runs.borrow(), 3);