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}
204
205/// Create a new error boundary component that catches any errors thrown from child components
206///
207/// ## Details
208///
209/// 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.
210/// 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
211/// in your components to provide a better user experience when you know that an error is likely to occur.
212///
213/// ## Example
214///
215/// ```rust, no_run
216/// use dioxus::prelude::*;
217///
218/// fn App() -> Element {
219///     let mut multiplier = use_signal(|| String::from("2"));
220///     rsx! {
221///         input {
222///             r#type: "text",
223///             value: multiplier,
224///             oninput: move |e| multiplier.set(e.value())
225///         }
226///         ErrorBoundary {
227///             handle_error: |errors: ErrorContext| {
228///                 rsx! {
229///                     div {
230///                         "Oops, we encountered an error. Please report {errors:?} to the developer of this application"
231///                     }
232///                 }
233///             },
234///             Counter {
235///                 multiplier
236///             }
237///         }
238///     }
239/// }
240///
241/// #[component]
242/// fn Counter(multiplier: ReadSignal<String>) -> Element {
243///     let multiplier_parsed = multiplier().parse::<usize>()?;
244///     let mut count = use_signal(|| multiplier_parsed);
245///     rsx! {
246///         button {
247///             onclick: move |_| {
248///                 let multiplier_parsed = multiplier().parse::<usize>()?;
249///                 *count.write() *= multiplier_parsed;
250///                 Ok(())
251///             },
252///             "{count}x{multiplier}"
253///         }
254///     }
255/// }
256/// ```
257///
258/// ## Resetting the error boundary
259///
260/// 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,
261/// you can call the [`ErrorContext::clear_errors`] method. This will clear all errors and re-render the children.
262///
263/// ```rust, no_run
264/// # use dioxus::prelude::*;
265/// fn App() -> Element {
266///     let mut multiplier = use_signal(|| String::new());
267///     rsx! {
268///         input {
269///             r#type: "text",
270///             value: multiplier,
271///             oninput: move |e| multiplier.set(e.value())
272///         }
273///         ErrorBoundary {
274///             handle_error: |errors: ErrorContext| {
275///                 rsx! {
276///                     div {
277///                         "Oops, we encountered an error. Please report {errors:?} to the developer of this application"
278///                     }
279///                     button {
280///                         onclick: move |_| {
281///                             errors.clear_errors();
282///                         },
283///                         "try again"
284///                     }
285///                 }
286///             },
287///             Counter {
288///                 multiplier
289///             }
290///         }
291///     }
292/// }
293///
294/// #[component]
295/// fn Counter(multiplier: ReadSignal<String>) -> Element {
296///     let multiplier_parsed = multiplier().parse::<usize>()?;
297///     let mut count = use_signal(|| multiplier_parsed);
298///     rsx! {
299///         button {
300///             onclick: move |_| {
301///                 let multiplier_parsed = multiplier().parse::<usize>()?;
302///                 *count.write() *= multiplier_parsed;
303///                 Ok(())
304///             },
305///             "{count}x{multiplier}"
306///         }
307///     }
308/// }
309/// ```
310#[allow(non_upper_case_globals, non_snake_case)]
311pub fn ErrorBoundary(props: ErrorBoundaryProps) -> Element {
312    let error_boundary = use_error_boundary_provider();
313    let errors = error_boundary.error();
314    let has_errors = errors.is_some();
315
316    // Drop errors before running user code that might borrow the error lock
317    drop(errors);
318
319    if has_errors {
320        (props.handle_error.0)(error_boundary.clone())
321    } else {
322        std::result::Result::Ok({
323            static TEMPLATE: Template = Template {
324                roots: &[TemplateNode::Dynamic { id: 0usize }],
325                node_paths: &[&[0u8]],
326                attr_paths: &[],
327            };
328            VNode::new(
329                None,
330                TEMPLATE,
331                Box::new([(props.children).into_dyn_node()]),
332                Default::default(),
333            )
334        })
335    }
336}
337
338impl ErrorBoundaryProps {
339    /**
340    Create a builder for building `ErrorBoundaryProps`.
341    On the builder, call `.children(...)`(optional), `.handle_error(...)`(optional) to set the values of the fields.
342    Finally, call `.build()` to create the instance of `ErrorBoundaryProps`.
343                        */
344    #[allow(dead_code)]
345    pub fn builder() -> ErrorBoundaryPropsBuilder<((), ())> {
346        ErrorBoundaryPropsBuilder { fields: ((), ()) }
347    }
348}
349
350#[must_use]
351#[doc(hidden)]
352#[allow(dead_code, non_camel_case_types, non_snake_case)]
353pub struct ErrorBoundaryPropsBuilder<TypedBuilderFields> {
354    fields: TypedBuilderFields,
355}
356impl<TypedBuilderFields> Clone for ErrorBoundaryPropsBuilder<TypedBuilderFields>
357where
358    TypedBuilderFields: Clone,
359{
360    fn clone(&self) -> Self {
361        Self {
362            fields: self.fields.clone(),
363        }
364    }
365}
366impl Properties for ErrorBoundaryProps {
367    type Builder = ErrorBoundaryPropsBuilder<((), ())>;
368    fn builder() -> Self::Builder {
369        ErrorBoundaryProps::builder()
370    }
371    fn memoize(&mut self, other: &Self) -> bool {
372        *self = other.clone();
373        false
374    }
375}
376#[doc(hidden)]
377#[allow(dead_code, non_camel_case_types, non_snake_case)]
378pub trait ErrorBoundaryPropsBuilder_Optional<T> {
379    fn into_value<F: FnOnce() -> T>(self, default: F) -> T;
380}
381impl<T> ErrorBoundaryPropsBuilder_Optional<T> for () {
382    fn into_value<F: FnOnce() -> T>(self, default: F) -> T {
383        default()
384    }
385}
386impl<T> ErrorBoundaryPropsBuilder_Optional<T> for (T,) {
387    fn into_value<F: FnOnce() -> T>(self, _: F) -> T {
388        self.0
389    }
390}
391#[allow(dead_code, non_camel_case_types, missing_docs)]
392impl<__handle_error> ErrorBoundaryPropsBuilder<((), __handle_error)> {
393    pub fn children(
394        self,
395        children: Element,
396    ) -> ErrorBoundaryPropsBuilder<((Element,), __handle_error)> {
397        let children = (children,);
398        let (_, handle_error) = self.fields;
399        ErrorBoundaryPropsBuilder {
400            fields: (children, handle_error),
401        }
402    }
403}
404#[doc(hidden)]
405#[allow(dead_code, non_camel_case_types, non_snake_case)]
406pub enum ErrorBoundaryPropsBuilder_Error_Repeated_field_children {}
407#[doc(hidden)]
408#[allow(dead_code, non_camel_case_types, missing_docs)]
409impl<__handle_error> ErrorBoundaryPropsBuilder<((Element,), __handle_error)> {
410    #[deprecated(note = "Repeated field children")]
411    pub fn children(
412        self,
413        _: ErrorBoundaryPropsBuilder_Error_Repeated_field_children,
414    ) -> ErrorBoundaryPropsBuilder<((Element,), __handle_error)> {
415        self
416    }
417}
418#[allow(dead_code, non_camel_case_types, missing_docs)]
419impl<__children> ErrorBoundaryPropsBuilder<(__children, ())> {
420    pub fn handle_error(
421        self,
422        handle_error: impl ::core::convert::Into<ErrorHandler>,
423    ) -> ErrorBoundaryPropsBuilder<(__children, (ErrorHandler,))> {
424        let handle_error = (handle_error.into(),);
425        let (children, _) = self.fields;
426        ErrorBoundaryPropsBuilder {
427            fields: (children, handle_error),
428        }
429    }
430}
431#[doc(hidden)]
432#[allow(dead_code, non_camel_case_types, non_snake_case)]
433pub enum ErrorBoundaryPropsBuilder_Error_Repeated_field_handle_error {}
434#[doc(hidden)]
435#[allow(dead_code, non_camel_case_types, missing_docs)]
436impl<__children> ErrorBoundaryPropsBuilder<(__children, (ErrorHandler,))> {
437    #[deprecated(note = "Repeated field handle_error")]
438    pub fn handle_error(
439        self,
440        _: ErrorBoundaryPropsBuilder_Error_Repeated_field_handle_error,
441    ) -> ErrorBoundaryPropsBuilder<(__children, (ErrorHandler,))> {
442        self
443    }
444}
445#[allow(dead_code, non_camel_case_types, missing_docs)]
446impl<
447        __handle_error: ErrorBoundaryPropsBuilder_Optional<ErrorHandler>,
448        __children: ErrorBoundaryPropsBuilder_Optional<Element>,
449    > ErrorBoundaryPropsBuilder<(__children, __handle_error)>
450{
451    pub fn build(self) -> ErrorBoundaryProps {
452        let (children, handle_error) = self.fields;
453        let children = ErrorBoundaryPropsBuilder_Optional::into_value(children, VNode::empty);
454        let handle_error = ErrorBoundaryPropsBuilder_Optional::into_value(handle_error, || {
455            ErrorHandler(Rc::new(default_handler))
456        });
457        ErrorBoundaryProps {
458            children,
459            handle_error,
460        }
461    }
462}