Skip to main content

dioxus_core/
error_boundary.rs

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