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}