Skip to main content

dioxus_core/
error_boundary.rs

1use crate::{
2    DynamicValues, Element, IntoDynNode, ReactiveContext, Subscribers, Template, VNode,
3    innerlude::{CapturedError, provide_context},
4    try_consume_context, use_hook,
5};
6use dioxus_core_template::{TemplateRawTree, TemplateStorage};
7use std::{
8    any::Any,
9    cell::RefCell,
10    fmt::{Debug, Display},
11    rc::Rc,
12};
13
14static ERROR_DYNAMIC_TREE: TemplateRawTree = TemplateRawTree::DynamicNode;
15
16/// Return early with an error.
17#[macro_export]
18macro_rules! bail {
19    ($msg:literal $(,)?) => {
20        return ::std::result::Result::Err($crate::internal::__anyhow!($msg).into())
21    };
22    ($err:expr $(,)?) => {
23        return ::std::result::Result::Err($crate::internal::__anyhow!($err).into())
24    };
25    ($fmt:expr, $($arg:tt)*) => {
26        return ::std::result::Result::Err($crate::internal::__anyhow!($fmt, $($arg)*).into())
27    };
28}
29
30/// A panic in a component that was caught by an error boundary.
31///
32/// <div class="warning">
33///
34/// WASM currently does not support caching unwinds, so this struct will not be created in WASM.
35///
36/// </div>
37pub(crate) struct CapturedPanic(pub(crate) Box<dyn Any + Send + 'static>);
38unsafe impl Sync for CapturedPanic {}
39impl std::error::Error for CapturedPanic {}
40impl Debug for CapturedPanic {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        f.debug_struct("CapturedPanic").finish()
43    }
44}
45
46impl Display for CapturedPanic {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        f.write_fmt(format_args!("Encountered panic: {:?}", self.0))
49    }
50}
51
52/// A context supplied by fullstack to create hydration compatible error boundaries. Generally, this
53/// is not present and the default in memory error boundary is used. If fullstack is enabled, it will
54/// provide its own factory that handles syncing errors to the hydration context
55#[derive(Clone, Copy)]
56struct CreateErrorBoundary(fn() -> ErrorContext);
57
58impl Default for CreateErrorBoundary {
59    fn default() -> Self {
60        Self(|| ErrorContext::new(None))
61    }
62}
63
64/// Provides a method that is used to create error boundaries in `use_error_boundary_provider`.
65/// This is only called from fullstack to create a hydration compatible error boundary
66pub fn provide_create_error_boundary(create_error_boundary: fn() -> ErrorContext) {
67    provide_context(CreateErrorBoundary(create_error_boundary));
68}
69
70/// Create an error boundary with the current error boundary factory (either hydration compatible or default)
71fn create_error_boundary() -> ErrorContext {
72    let create_error_boundary = try_consume_context::<CreateErrorBoundary>().unwrap_or_default();
73    (create_error_boundary.0)()
74}
75
76/// Provide an error boundary to catch errors from child components. This needs to called in a hydration comptable
77/// order if fullstack is enabled
78pub fn use_error_boundary_provider() -> ErrorContext {
79    use_hook(|| provide_context(create_error_boundary()))
80}
81
82/// A context with information about suspended components
83#[derive(Clone)]
84pub struct ErrorContext {
85    error: Rc<RefCell<Option<CapturedError>>>,
86    subscribers: Subscribers,
87}
88
89impl Debug for ErrorContext {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        f.debug_struct("ErrorContext")
92            .field("error", &self.error)
93            .finish()
94    }
95}
96
97impl PartialEq for ErrorContext {
98    fn eq(&self, other: &Self) -> bool {
99        Rc::ptr_eq(&self.error, &other.error)
100    }
101}
102
103impl ErrorContext {
104    /// Create a new suspense boundary in a specific scope
105    pub fn new(error: Option<CapturedError>) -> Self {
106        Self {
107            error: Rc::new(RefCell::new(error)),
108            subscribers: Subscribers::new(),
109        }
110    }
111
112    /// Get the current error, if any. If multiple components have errored, this will return the first
113    /// error that made it to this boundary.
114    pub fn error(&self) -> Option<CapturedError> {
115        // Subscribe to the current reactive context if one exists. This is usually
116        // the error boundary component that is rendering the errors
117        if let Some(rc) = ReactiveContext::current() {
118            self.subscribers.add(rc);
119        }
120
121        self.error.borrow().clone()
122    }
123
124    /// Push an error into this Error Boundary
125    pub fn insert_error(&self, error: CapturedError) {
126        self.error.borrow_mut().replace(error);
127        self.mark_dirty()
128    }
129
130    /// Clear all errors from this Error Boundary
131    pub fn clear_errors(&self) {
132        self.error.borrow_mut().take();
133        self.mark_dirty();
134    }
135
136    /// Mark the error context as dirty and notify all subscribers
137    fn mark_dirty(&self) {
138        let mut this_subscribers_vec = Vec::new();
139        self.subscribers
140            .visit(|subscriber| this_subscribers_vec.push(*subscriber));
141        for subscriber in this_subscribers_vec {
142            self.subscribers.remove(&subscriber);
143            subscriber.mark_dirty();
144        }
145    }
146}
147
148#[allow(clippy::type_complexity)]
149#[derive(Clone)]
150pub struct ErrorHandler(Rc<dyn Fn(ErrorContext) -> Element>);
151impl<F: Fn(ErrorContext) -> Element + 'static> From<F> for ErrorHandler {
152    fn from(value: F) -> Self {
153        Self(Rc::new(value))
154    }
155}
156
157fn default_handler(errors: ErrorContext) -> Element {
158    static ATTRS: TemplateRawTree = TemplateRawTree::StaticAttr {
159        name: "color",
160        value: "red",
161        namespace: Some("style"),
162    };
163    static TREE: TemplateRawTree = TemplateRawTree::Element {
164        tag: "div",
165        namespace: None,
166        attrs: &ATTRS,
167        children: &ERROR_DYNAMIC_TREE,
168    };
169    static STORAGE: TemplateStorage<8, 4, 2> = TemplateStorage::build_from_tree(&TREE);
170    static TEMPLATE: Template = STORAGE.as_template();
171
172    std::result::Result::Ok(VNode::new(
173        TEMPLATE,
174        DynamicValues::from_parts(
175            None,
176            Box::new([errors
177                .error()
178                .iter()
179                .map(|e| {
180                    static TREE: TemplateRawTree = TemplateRawTree::Element {
181                        tag: "pre",
182                        namespace: None,
183                        attrs: &TemplateRawTree::Empty,
184                        children: &ERROR_DYNAMIC_TREE,
185                    };
186                    static STORAGE: TemplateStorage<4, 1, 2> =
187                        TemplateStorage::build_from_tree(&TREE);
188                    static INNER_TEMPLATE: Template = STORAGE.as_template();
189
190                    VNode::new(
191                        INNER_TEMPLATE,
192                        DynamicValues::from_parts(
193                            None,
194                            Box::new([e.to_string().into_dyn_node()]),
195                            Box::new([]),
196                        ),
197                    )
198                })
199                .into_dyn_node()]),
200            Box::new([]),
201        ),
202    ))
203}
204
205#[derive(dioxus_core_macro::Props, Clone)]
206pub struct ErrorBoundaryProps {
207    children: Element,
208    #[props(into, default = ErrorHandler(Rc::new(default_handler)))]
209    handle_error: ErrorHandler,
210}
211
212// The `Props` derive needs the props to be `PartialEq` for memoization, but `ErrorHandler` wraps an
213// `Rc<dyn Fn>` that can't be compared structurally. Hand-write it: memoize on the children and the
214// handler's identity (pointer equality).
215impl PartialEq for ErrorBoundaryProps {
216    fn eq(&self, other: &Self) -> bool {
217        self.children == other.children && Rc::ptr_eq(&self.handle_error.0, &other.handle_error.0)
218    }
219}
220
221/// Create a new error boundary component that catches any errors thrown from child components
222///
223/// ## Details
224///
225/// Error boundaries handle errors within a specific part of your application. They are similar to `try/catch` in JavaScript, but they only catch errors in the tree below them.
226/// Any errors passed up from a child will be caught by the nearest error boundary. Error boundaries are quick to implement, but it can be useful to individually handle errors
227/// in your components to provide a better user experience when you know that an error is likely to occur.
228///
229/// ## Example
230///
231/// ```rust, no_run
232/// use dioxus::prelude::*;
233///
234/// fn App() -> Element {
235///     let mut multiplier = use_signal(|| String::from("2"));
236///     rsx! {
237///         input {
238///             r#type: "text",
239///             value: multiplier,
240///             oninput: move |e| multiplier.set(e.value())
241///         }
242///         ErrorBoundary {
243///             handle_error: |errors: ErrorContext| {
244///                 rsx! {
245///                     div {
246///                         "Oops, we encountered an error. Please report {errors:?} to the developer of this application"
247///                     }
248///                 }
249///             },
250///             Counter {
251///                 multiplier
252///             }
253///         }
254///     }
255/// }
256///
257/// #[component]
258/// fn Counter(multiplier: ReadSignal<String>) -> Element {
259///     let multiplier_parsed = multiplier().parse::<usize>()?;
260///     let mut count = use_signal(|| multiplier_parsed);
261///     rsx! {
262///         button {
263///             onclick: move |_| {
264///                 let multiplier_parsed = multiplier().parse::<usize>()?;
265///                 *count.write() *= multiplier_parsed;
266///                 Ok(())
267///             },
268///             "{count}x{multiplier}"
269///         }
270///     }
271/// }
272/// ```
273///
274/// ## Resetting the error boundary
275///
276/// Once the error boundary catches an error, it will render the rsx returned from the handle_error function instead of the children. To reset the error boundary,
277/// you can call the [`ErrorContext::clear_errors`] method. This will clear all errors and re-render the children.
278///
279/// ```rust, no_run
280/// # use dioxus::prelude::*;
281/// fn App() -> Element {
282///     let mut multiplier = use_signal(|| String::new());
283///     rsx! {
284///         input {
285///             r#type: "text",
286///             value: multiplier,
287///             oninput: move |e| multiplier.set(e.value())
288///         }
289///         ErrorBoundary {
290///             handle_error: |errors: ErrorContext| {
291///                 rsx! {
292///                     div {
293///                         "Oops, we encountered an error. Please report {errors:?} to the developer of this application"
294///                     }
295///                     button {
296///                         onclick: move |_| {
297///                             errors.clear_errors();
298///                         },
299///                         "try again"
300///                     }
301///                 }
302///             },
303///             Counter {
304///                 multiplier
305///             }
306///         }
307///     }
308/// }
309///
310/// #[component]
311/// fn Counter(multiplier: ReadSignal<String>) -> Element {
312///     let multiplier_parsed = multiplier().parse::<usize>()?;
313///     let mut count = use_signal(|| multiplier_parsed);
314///     rsx! {
315///         button {
316///             onclick: move |_| {
317///                 let multiplier_parsed = multiplier().parse::<usize>()?;
318///                 *count.write() *= multiplier_parsed;
319///                 Ok(())
320///             },
321///             "{count}x{multiplier}"
322///         }
323///     }
324/// }
325/// ```
326#[allow(non_upper_case_globals, non_snake_case)]
327pub fn ErrorBoundary(props: ErrorBoundaryProps) -> Element {
328    let error_boundary = use_error_boundary_provider();
329    let errors = error_boundary.error();
330    let has_errors = errors.is_some();
331
332    // Drop errors before running user code that might borrow the error lock
333    drop(errors);
334
335    if has_errors {
336        (props.handle_error.0)(error_boundary.clone())
337    } else {
338        std::result::Result::Ok({
339            static STORAGE: TemplateStorage<1, 1, 1> =
340                TemplateStorage::build_from_tree(&ERROR_DYNAMIC_TREE);
341            static TEMPLATE: Template = STORAGE.as_template();
342
343            VNode::new(
344                TEMPLATE,
345                DynamicValues::from_parts(
346                    None,
347                    Box::new([(props.children).into_dyn_node()]),
348                    Box::new([]),
349                ),
350            )
351        })
352    }
353}