dioxus_core/
error_boundary.rs

1use crate::{
2    innerlude::{provide_context, CapturedError},
3    try_consume_context, use_hook, Element, IntoDynNode, Properties, ReactiveContext, Subscribers,
4    Template, TemplateAttribute, TemplateNode, VNode,
5};
6use std::{
7    any::Any,
8    cell::RefCell,
9    fmt::{Debug, Display},
10    rc::Rc,
11};
12
13/// Return early with an error.
14#[macro_export]
15macro_rules! bail {
16    ($msg:literal $(,)?) => {
17        return $crate::internal::Err($crate::internal::__anyhow!($msg).into())
18    };
19    ($err:expr $(,)?) => {
20        return $crate::internal::Err($crate::internal::__anyhow!($err).into())
21    };
22    ($fmt:expr, $($arg:tt)*) => {
23        return $crate::internal::Err($crate::internal::__anyhow!($fmt, $($arg)*).into())
24    };
25}
26
27/// A panic in a component that was caught by an error boundary.
28///
29/// <div class="warning">
30///
31/// WASM currently does not support caching unwinds, so this struct will not be created in WASM.
32///
33/// </div>
34pub(crate) struct CapturedPanic(pub(crate) Box<dyn Any + Send + 'static>);
35unsafe impl Sync for CapturedPanic {}
36impl std::error::Error for CapturedPanic {}
37impl Debug for CapturedPanic {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        f.debug_struct("CapturedPanic").finish()
40    }
41}
42
43impl Display for CapturedPanic {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        f.write_fmt(format_args!("Encountered panic: {:?}", self.0))
46    }
47}
48
49/// A context supplied by fullstack to create hydration compatible error boundaries. Generally, this
50/// is not present and the default in memory error boundary is used. If fullstack is enabled, it will
51/// provide its own factory that handles syncing errors to the hydration context
52#[derive(Clone, Copy)]
53struct CreateErrorBoundary(fn() -> ErrorContext);
54
55impl Default for CreateErrorBoundary {
56    fn default() -> Self {
57        Self(|| ErrorContext::new(None))
58    }
59}
60
61/// Provides a method that is used to create error boundaries in `use_error_boundary_provider`.
62/// This is only called from fullstack to create a hydration compatible error boundary
63#[doc(hidden)]
64pub fn provide_create_error_boundary(create_error_boundary: fn() -> ErrorContext) {
65    provide_context(CreateErrorBoundary(create_error_boundary));
66}
67
68/// Create an error boundary with the current error boundary factory (either hydration compatible or default)
69fn create_error_boundary() -> ErrorContext {
70    let create_error_boundary = try_consume_context::<CreateErrorBoundary>().unwrap_or_default();
71    (create_error_boundary.0)()
72}
73
74/// Provide an error boundary to catch errors from child components. This needs to called in a hydration comptable
75/// order if fullstack is enabled
76pub fn use_error_boundary_provider() -> ErrorContext {
77    use_hook(|| provide_context(create_error_boundary()))
78}
79
80/// A context with information about suspended components
81#[derive(Clone)]
82pub struct ErrorContext {
83    error: Rc<RefCell<Option<CapturedError>>>,
84    subscribers: Subscribers,
85}
86
87impl Debug for ErrorContext {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        f.debug_struct("ErrorContext")
90            .field("error", &self.error)
91            .finish()
92    }
93}
94
95impl PartialEq for ErrorContext {
96    fn eq(&self, other: &Self) -> bool {
97        Rc::ptr_eq(&self.error, &other.error)
98    }
99}
100
101impl ErrorContext {
102    /// Create a new suspense boundary in a specific scope
103    pub fn new(error: Option<CapturedError>) -> Self {
104        Self {
105            error: Rc::new(RefCell::new(error)),
106            subscribers: Subscribers::new(),
107        }
108    }
109
110    /// Get the current error, if any. If multiple components have errored, this will return the first
111    /// error that made it to this boundary.
112    pub fn error(&self) -> Option<CapturedError> {
113        // Subscribe to the current reactive context if one exists. This is usually
114        // the error boundary component that is rendering the errors
115        if let Some(rc) = ReactiveContext::current() {
116            self.subscribers.add(rc);
117        }
118
119        self.error.borrow().clone()
120    }
121
122    /// Push an error into this Error Boundary
123    pub fn insert_error(&self, error: CapturedError) {
124        self.error.borrow_mut().replace(error);
125        self.mark_dirty()
126    }
127
128    /// Clear all errors from this Error Boundary
129    pub fn clear_errors(&self) {
130        self.error.borrow_mut().take();
131        self.mark_dirty();
132    }
133
134    /// Mark the error context as dirty and notify all subscribers
135    fn mark_dirty(&self) {
136        let mut this_subscribers_vec = Vec::new();
137        self.subscribers
138            .visit(|subscriber| this_subscribers_vec.push(*subscriber));
139        for subscriber in this_subscribers_vec {
140            self.subscribers.remove(&subscriber);
141            subscriber.mark_dirty();
142        }
143    }
144}
145
146#[allow(clippy::type_complexity)]
147#[derive(Clone)]
148pub struct ErrorHandler(Rc<dyn Fn(ErrorContext) -> Element>);
149impl<F: Fn(ErrorContext) -> Element + 'static> From<F> for ErrorHandler {
150    fn from(value: F) -> Self {
151        Self(Rc::new(value))
152    }
153}
154
155fn default_handler(errors: ErrorContext) -> Element {
156    static TEMPLATE: Template = Template {
157        roots: &[TemplateNode::Element {
158            tag: "div",
159            namespace: None,
160            attrs: &[TemplateAttribute::Static {
161                name: "color",
162                namespace: Some("style"),
163                value: "red",
164            }],
165            children: &[TemplateNode::Dynamic { id: 0usize }],
166        }],
167        node_paths: &[&[0u8, 0u8]],
168        attr_paths: &[],
169    };
170    std::result::Result::Ok(VNode::new(
171        None,
172        TEMPLATE,
173        Box::new([errors
174            .error()
175            .iter()
176            .map(|e| {
177                static TEMPLATE: Template = Template {
178                    roots: &[TemplateNode::Element {
179                        tag: "pre",
180                        namespace: None,
181                        attrs: &[],
182                        children: &[TemplateNode::Dynamic { id: 0usize }],
183                    }],
184                    node_paths: &[&[0u8, 0u8]],
185                    attr_paths: &[],
186                };
187                VNode::new(
188                    None,
189                    TEMPLATE,
190                    Box::new([e.to_string().into_dyn_node()]),
191                    Default::default(),
192                )
193            })
194            .into_dyn_node()]),
195        Default::default(),
196    ))
197}
198
199#[derive(Clone)]
200pub struct ErrorBoundaryProps {
201    children: Element,
202    handle_error: ErrorHandler,
203}
204impl ErrorBoundaryProps {
205    /**
206    Create a builder for building `ErrorBoundaryProps`.
207    On the builder, call `.children(...)`(optional), `.handle_error(...)`(optional) to set the values of the fields.
208    Finally, call `.build()` to create the instance of `ErrorBoundaryProps`.
209                        */
210    #[allow(dead_code)]
211    pub fn builder() -> ErrorBoundaryPropsBuilder<((), ())> {
212        ErrorBoundaryPropsBuilder { fields: ((), ()) }
213    }
214}
215
216#[must_use]
217#[doc(hidden)]
218#[allow(dead_code, non_camel_case_types, non_snake_case)]
219pub struct ErrorBoundaryPropsBuilder<TypedBuilderFields> {
220    fields: TypedBuilderFields,
221}
222impl<TypedBuilderFields> Clone for ErrorBoundaryPropsBuilder<TypedBuilderFields>
223where
224    TypedBuilderFields: Clone,
225{
226    fn clone(&self) -> Self {
227        Self {
228            fields: self.fields.clone(),
229        }
230    }
231}
232impl Properties for ErrorBoundaryProps {
233    type Builder = ErrorBoundaryPropsBuilder<((), ())>;
234    fn builder() -> Self::Builder {
235        ErrorBoundaryProps::builder()
236    }
237    fn memoize(&mut self, other: &Self) -> bool {
238        *self = other.clone();
239        false
240    }
241}
242#[doc(hidden)]
243#[allow(dead_code, non_camel_case_types, non_snake_case)]
244pub trait ErrorBoundaryPropsBuilder_Optional<T> {
245    fn into_value<F: FnOnce() -> T>(self, default: F) -> T;
246}
247impl<T> ErrorBoundaryPropsBuilder_Optional<T> for () {
248    fn into_value<F: FnOnce() -> T>(self, default: F) -> T {
249        default()
250    }
251}
252impl<T> ErrorBoundaryPropsBuilder_Optional<T> for (T,) {
253    fn into_value<F: FnOnce() -> T>(self, _: F) -> T {
254        self.0
255    }
256}
257#[allow(dead_code, non_camel_case_types, missing_docs)]
258impl<__handle_error> ErrorBoundaryPropsBuilder<((), __handle_error)> {
259    pub fn children(
260        self,
261        children: Element,
262    ) -> ErrorBoundaryPropsBuilder<((Element,), __handle_error)> {
263        let children = (children,);
264        let (_, handle_error) = self.fields;
265        ErrorBoundaryPropsBuilder {
266            fields: (children, handle_error),
267        }
268    }
269}
270#[doc(hidden)]
271#[allow(dead_code, non_camel_case_types, non_snake_case)]
272pub enum ErrorBoundaryPropsBuilder_Error_Repeated_field_children {}
273#[doc(hidden)]
274#[allow(dead_code, non_camel_case_types, missing_docs)]
275impl<__handle_error> ErrorBoundaryPropsBuilder<((Element,), __handle_error)> {
276    #[deprecated(note = "Repeated field children")]
277    pub fn children(
278        self,
279        _: ErrorBoundaryPropsBuilder_Error_Repeated_field_children,
280    ) -> ErrorBoundaryPropsBuilder<((Element,), __handle_error)> {
281        self
282    }
283}
284#[allow(dead_code, non_camel_case_types, missing_docs)]
285impl<__children> ErrorBoundaryPropsBuilder<(__children, ())> {
286    pub fn handle_error(
287        self,
288        handle_error: impl ::core::convert::Into<ErrorHandler>,
289    ) -> ErrorBoundaryPropsBuilder<(__children, (ErrorHandler,))> {
290        let handle_error = (handle_error.into(),);
291        let (children, _) = self.fields;
292        ErrorBoundaryPropsBuilder {
293            fields: (children, handle_error),
294        }
295    }
296}
297#[doc(hidden)]
298#[allow(dead_code, non_camel_case_types, non_snake_case)]
299pub enum ErrorBoundaryPropsBuilder_Error_Repeated_field_handle_error {}
300#[doc(hidden)]
301#[allow(dead_code, non_camel_case_types, missing_docs)]
302impl<__children> ErrorBoundaryPropsBuilder<(__children, (ErrorHandler,))> {
303    #[deprecated(note = "Repeated field handle_error")]
304    pub fn handle_error(
305        self,
306        _: ErrorBoundaryPropsBuilder_Error_Repeated_field_handle_error,
307    ) -> ErrorBoundaryPropsBuilder<(__children, (ErrorHandler,))> {
308        self
309    }
310}
311#[allow(dead_code, non_camel_case_types, missing_docs)]
312impl<
313        __handle_error: ErrorBoundaryPropsBuilder_Optional<ErrorHandler>,
314        __children: ErrorBoundaryPropsBuilder_Optional<Element>,
315    > ErrorBoundaryPropsBuilder<(__children, __handle_error)>
316{
317    pub fn build(self) -> ErrorBoundaryProps {
318        let (children, handle_error) = self.fields;
319        let children = ErrorBoundaryPropsBuilder_Optional::into_value(children, VNode::empty);
320        let handle_error = ErrorBoundaryPropsBuilder_Optional::into_value(handle_error, || {
321            ErrorHandler(Rc::new(default_handler))
322        });
323        ErrorBoundaryProps {
324            children,
325            handle_error,
326        }
327    }
328}
329
330/// Create a new error boundary component that catches any errors thrown from child components
331///
332/// ## Details
333///
334/// 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.
335/// 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
336/// in your components to provide a better user experience when you know that an error is likely to occur.
337///
338/// ## Example
339///
340/// ```rust, no_run
341/// use dioxus::prelude::*;
342///
343/// fn App() -> Element {
344///     let mut multiplier = use_signal(|| String::from("2"));
345///     rsx! {
346///         input {
347///             r#type: "text",
348///             value: multiplier,
349///             oninput: move |e| multiplier.set(e.value())
350///         }
351///         ErrorBoundary {
352///             handle_error: |errors: ErrorContext| {
353///                 rsx! {
354///                     div {
355///                         "Oops, we encountered an error. Please report {errors:?} to the developer of this application"
356///                     }
357///                 }
358///             },
359///             Counter {
360///                 multiplier
361///             }
362///         }
363///     }
364/// }
365///
366/// #[component]
367/// fn Counter(multiplier: ReadSignal<String>) -> Element {
368///     let multiplier_parsed = multiplier().parse::<usize>()?;
369///     let mut count = use_signal(|| multiplier_parsed);
370///     rsx! {
371///         button {
372///             onclick: move |_| {
373///                 let multiplier_parsed = multiplier().parse::<usize>()?;
374///                 *count.write() *= multiplier_parsed;
375///                 Ok(())
376///             },
377///             "{count}x{multiplier}"
378///         }
379///     }
380/// }
381/// ```
382///
383/// ## Resetting the error boundary
384///
385/// 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,
386/// you can call the [`ErrorContext::clear_errors`] method. This will clear all errors and re-render the children.
387///
388/// ```rust, no_run
389/// # use dioxus::prelude::*;
390/// fn App() -> Element {
391///     let mut multiplier = use_signal(|| String::new());
392///     rsx! {
393///         input {
394///             r#type: "text",
395///             value: multiplier,
396///             oninput: move |e| multiplier.set(e.value())
397///         }
398///         ErrorBoundary {
399///             handle_error: |errors: ErrorContext| {
400///                 rsx! {
401///                     div {
402///                         "Oops, we encountered an error. Please report {errors:?} to the developer of this application"
403///                     }
404///                     button {
405///                         onclick: move |_| {
406///                             errors.clear_errors();
407///                         },
408///                         "try again"
409///                     }
410///                 }
411///             },
412///             Counter {
413///                 multiplier
414///             }
415///         }
416///     }
417/// }
418///
419/// #[component]
420/// fn Counter(multiplier: ReadSignal<String>) -> Element {
421///     let multiplier_parsed = multiplier().parse::<usize>()?;
422///     let mut count = use_signal(|| multiplier_parsed);
423///     rsx! {
424///         button {
425///             onclick: move |_| {
426///                 let multiplier_parsed = multiplier().parse::<usize>()?;
427///                 *count.write() *= multiplier_parsed;
428///                 Ok(())
429///             },
430///             "{count}x{multiplier}"
431///         }
432///     }
433/// }
434/// ```
435#[allow(non_upper_case_globals, non_snake_case)]
436pub fn ErrorBoundary(props: ErrorBoundaryProps) -> Element {
437    let error_boundary = use_error_boundary_provider();
438    let errors = error_boundary.error();
439    let has_errors = errors.is_some();
440
441    // Drop errors before running user code that might borrow the error lock
442    drop(errors);
443
444    if has_errors {
445        (props.handle_error.0)(error_boundary.clone())
446    } else {
447        std::result::Result::Ok({
448            static TEMPLATE: Template = Template {
449                roots: &[TemplateNode::Dynamic { id: 0usize }],
450                node_paths: &[&[0u8]],
451                attr_paths: &[],
452            };
453            VNode::new(
454                None,
455                TEMPLATE,
456                Box::new([(props.children).into_dyn_node()]),
457                Default::default(),
458            )
459        })
460    }
461}