Skip to main content

scope_catch

Function scope_catch 

Source
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 Result values 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 f itself — 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 unowned is outside every boundary above it and its panics propagate as usual. That is the same opt-out unowned already applies to disposal; take an Owner and run_in to 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);