diff --git a/Cargo.toml b/Cargo.toml index 29ae2d30d..3941298bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -133,4 +133,8 @@ fern = { version = "0.6.0", features = ["colored"] } env_logger = "0.10.0" simple_logger = "4.0.0" thiserror = { workspace = true } + + +[dependencies] +tracing-subscriber = "0.3.17" http-range = "0.1.5" diff --git a/examples/error_handle.rs b/examples/error_handle.rs index d1fb57933..21b16ec45 100644 --- a/examples/error_handle.rs +++ b/examples/error_handle.rs @@ -1,4 +1,4 @@ -use dioxus::prelude::*; +use dioxus::{core::CapturedError, prelude::*}; fn main() { dioxus_desktop::launch(App); @@ -6,30 +6,25 @@ fn main() { #[component] fn App(cx: Scope) -> Element { - let val = use_state(cx, || "0.0001"); - - let num = match val.parse::() { - Err(_) => return cx.render(rsx!("Parsing failed")), - Ok(num) => num, - }; - cx.render(rsx! { - h1 { "The parsed value is {num}" } - button { - onclick: move |_| val.set("invalid"), - "Set an invalid number" + ErrorBoundary { + handle_error: |error: CapturedError| rsx! {"Found error {error}"}, + DemoC { + x: 1 + } } - (0..5).map(|i| rsx! { - DemoC { x: i } - }) }) } #[component] fn DemoC(cx: Scope, x: i32) -> Element { + let result = Err("Error"); + + result.throw()?; + cx.render(rsx! { h1 { - "asdasdasdasd {x}" + "{x}" } }) } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 58bd934b9..43b27f2e7 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -602,7 +602,7 @@ impl<'b> VirtualDom { // If none of the old keys are reused by the new children, then we remove all the remaining old children and // create the new children afresh. if shared_keys.is_empty() { - if old.first().is_some() { + if !old.is_empty() { self.remove_nodes(&old[1..]); self.replace(&old[0], new, Some(parent)); } else { diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index 819d2f99e..2f64bea9f 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -1,25 +1,68 @@ -use crate::{ScopeId, ScopeState}; +use crate::{ + scope_context::{consume_context, current_scope_id, schedule_update_any}, + Element, IntoDynNode, LazyNodes, Properties, Scope, ScopeId, ScopeState, Template, + TemplateAttribute, TemplateNode, VNode, +}; use std::{ any::{Any, TypeId}, + backtrace::Backtrace, cell::RefCell, - fmt::Debug, + error::Error, + fmt::{Debug, Display}, + rc::Rc, + sync::Arc, }; -/// A boundary that will capture any errors from child components -pub struct ErrorBoundary { - error: RefCell>, - _id: ScopeId, +/// Provide an error boundary to catch errors from child components +pub fn use_error_boundary(cx: &ScopeState) -> &ErrorBoundary { + cx.use_hook(|| cx.provide_context(ErrorBoundary::new())) } +/// A boundary that will capture any errors from child components +#[derive(Debug, Clone, Default)] +pub struct ErrorBoundary { + inner: Rc, +} + +/// A boundary that will capture any errors from child components +pub struct ErrorBoundaryInner { + error: RefCell>, + _id: ScopeId, + rerun_boundary: Arc, +} + +impl Debug for ErrorBoundaryInner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ErrorBoundaryInner") + .field("error", &self.error) + .finish() + } +} + +#[derive(Debug)] /// An instance of an error captured by a descendant component. pub struct CapturedError { /// The error captured by the error boundary pub error: Box, + /// The backtrace of the error + pub backtrace: Backtrace, + /// The scope that threw the error pub scope: ScopeId, } +impl Display for CapturedError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "Encountered error: {:?}\nIn scope: {:?}\nBacktrace: {}", + self.error, self.scope, self.backtrace + )) + } +} + +impl Error for CapturedError {} + impl CapturedError { /// Downcast the error type into a concrete error type pub fn downcast(&self) -> Option<&T> { @@ -32,17 +75,56 @@ impl CapturedError { } } -impl ErrorBoundary { - pub fn new(id: ScopeId) -> Self { +impl Default for ErrorBoundaryInner { + fn default() -> Self { Self { error: RefCell::new(None), - _id: id, + _id: current_scope_id() + .expect("Cannot create an error boundary outside of a component's scope."), + rerun_boundary: schedule_update_any().unwrap(), + } + } +} + +impl ErrorBoundary { + /// Create a new error boundary + pub fn new() -> Self { + Self::default() + } + + /// Create a new error boundary in the current scope + pub(crate) fn new_in_scope( + scope: ScopeId, + rerun_boundary: Arc, + ) -> Self { + Self { + inner: Rc::new(ErrorBoundaryInner { + error: RefCell::new(None), + _id: scope, + rerun_boundary, + }), } } /// Push an error into this Error Boundary - pub fn insert_error(&self, scope: ScopeId, error: Box) { - self.error.replace(Some(CapturedError { error, scope })); + pub fn insert_error( + &self, + scope: ScopeId, + error: Box, + backtrace: Backtrace, + ) { + println!("{:?} {:?}", error, self.inner._id); + self.inner.error.replace(Some(CapturedError { + error, + scope, + backtrace, + })); + (self.inner.rerun_boundary)(self.inner._id); + } + + /// Take any error that has been captured by this error boundary + pub fn take_error(&self) -> Option { + self.inner.error.take() } } @@ -59,7 +141,7 @@ impl ErrorBoundary { /// ```rust, ignore /// #[component] /// fn App(cx: Scope, count: String) -> Element { -/// let id: i32 = count.parse().throw(cx)?; +/// let id: i32 = count.parse().throw()?; /// /// cx.render(rsx! { /// div { "Count {}" } @@ -84,14 +166,14 @@ pub trait Throw: Sized { /// ```rust, ignore /// #[component] /// fn App(cx: Scope, count: String) -> Element { - /// let id: i32 = count.parse().throw(cx)?; + /// let id: i32 = count.parse().throw()?; /// /// cx.render(rsx! { /// div { "Count {}" } /// }) /// } /// ``` - fn throw(self, cx: &ScopeState) -> Option; + fn throw(self) -> Option; /// Returns an option that evaluates to None if there is an error, injecting the error to the nearest error boundary. /// @@ -107,45 +189,46 @@ pub trait Throw: Sized { /// ```rust, ignore /// #[component] /// fn App(cx: Scope, count: String) -> Element { - /// let id: i32 = count.parse().throw(cx)?; + /// let id: i32 = count.parse().throw()?; /// /// cx.render(rsx! { /// div { "Count {}" } /// }) /// } /// ``` - fn throw_with( - self, - cx: &ScopeState, - e: impl FnOnce() -> D, - ) -> Option; + fn throw_with(self, e: impl FnOnce() -> D) -> Option { + self.throw().or_else(|| throw_error(e())) + } +} + +fn throw_error(e: impl Debug + 'static) -> Option { + if let Some(cx) = consume_context::() { + match current_scope_id() { + Some(id) => cx.insert_error(id, Box::new(e), Backtrace::capture()), + None => { + tracing::error!("Cannot throw error outside of a component's scope.") + } + } + } + + None } /// We call clone on any errors that can be owned out of a reference impl<'a, T, O: Debug + 'static, E: ToOwned> Throw for &'a Result { type Out = &'a T; - fn throw(self, cx: &ScopeState) -> Option { + fn throw(self) -> Option { match self { Ok(t) => Some(t), - Err(e) => { - cx.throw(e.to_owned()); - None - } + Err(e) => throw_error(e.to_owned()), } } - fn throw_with( - self, - cx: &ScopeState, - err: impl FnOnce() -> D, - ) -> Option { + fn throw_with(self, err: impl FnOnce() -> D) -> Option { match self { Ok(t) => Some(t), - Err(_e) => { - cx.throw(err()); - None - } + Err(_e) => throw_error(err()), } } } @@ -154,25 +237,15 @@ impl<'a, T, O: Debug + 'static, E: ToOwned> Throw for &'a Result Throw for Result { type Out = T; - fn throw(self, cx: &ScopeState) -> Option { + fn throw(self) -> Option { match self { Ok(t) => Some(t), - Err(e) => { - cx.throw(e); - None - } + Err(e) => throw_error(e), } } - fn throw_with( - self, - cx: &ScopeState, - error: impl FnOnce() -> D, - ) -> Option { - self.ok().or_else(|| { - cx.throw(error()); - None - }) + fn throw_with(self, error: impl FnOnce() -> D) -> Option { + self.ok().or_else(|| throw_error(error())) } } @@ -180,21 +253,234 @@ impl Throw for Result { impl Throw for Option { type Out = T; - fn throw(self, cx: &ScopeState) -> Option { - self.or_else(|| { - cx.throw("None error."); - None - }) + fn throw(self) -> Option { + self.or_else(|| throw_error("Attempted to unwrap a None value.")) } - fn throw_with( - self, - cx: &ScopeState, - error: impl FnOnce() -> D, - ) -> Option { - self.or_else(|| { - cx.throw(error()); - None - }) + fn throw_with(self, error: impl FnOnce() -> D) -> Option { + self.or_else(|| throw_error(error())) + } +} + +pub struct ErrorHandler<'a>(Box LazyNodes<'a, 'a> + 'a>); +impl<'a, F: Fn(CapturedError) -> LazyNodes<'a, 'a> + 'a> From for ErrorHandler<'a> { + fn from(value: F) -> Self { + Self(Box::new(value)) + } +} +fn default_handler<'a>(error: CapturedError) -> LazyNodes<'a, 'a> { + LazyNodes::new(move |__cx: &ScopeState| -> VNode { + static TEMPLATE: Template = Template { + name: "error_handle.rs:42:5:884", + roots: &[TemplateNode::Element { + tag: "pre", + namespace: None, + attrs: &[TemplateAttribute::Static { + name: "color", + namespace: Some("style"), + value: "red", + }], + children: &[TemplateNode::DynamicText { id: 0usize }], + }], + node_paths: &[&[0u8, 0u8]], + attr_paths: &[], + }; + VNode { + parent: None, + key: None, + template: std::cell::Cell::new(TEMPLATE), + root_ids: bumpalo::collections::Vec::with_capacity_in(1usize, __cx.bump()).into(), + dynamic_nodes: __cx + .bump() + .alloc([__cx.text_node(format_args!("{0}", error))]), + dynamic_attrs: __cx.bump().alloc([]), + } + }) +} +pub struct ErrorBoundaryProps<'a> { + children: Element<'a>, + handle_error: ErrorHandler<'a>, +} +impl<'a> ErrorBoundaryProps<'a> { + /** + Create a builder for building `ErrorBoundaryProps`. + On the builder, call `.children(...)`(optional), `.handle_error(...)`(optional) to set the values of the fields. + Finally, call `.build()` to create the instance of `ErrorBoundaryProps`. + */ + #[allow(dead_code)] + pub fn builder() -> ErrorBoundaryPropsBuilder<'a, ((), ())> { + ErrorBoundaryPropsBuilder { + fields: ((), ()), + _phantom: ::core::default::Default::default(), + } + } +} +#[must_use] +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, non_snake_case)] +pub struct ErrorBoundaryPropsBuilder<'a, TypedBuilderFields> { + fields: TypedBuilderFields, + _phantom: ::core::marker::PhantomData<&'a ()>, +} +impl<'a, TypedBuilderFields> Clone for ErrorBoundaryPropsBuilder<'a, TypedBuilderFields> +where + TypedBuilderFields: Clone, +{ + fn clone(&self) -> Self { + Self { + fields: self.fields.clone(), + _phantom: ::core::default::Default::default(), + } + } +} +impl<'a> Properties for ErrorBoundaryProps<'a> { + type Builder = ErrorBoundaryPropsBuilder<'a, ((), ())>; + const IS_STATIC: bool = false; + fn builder() -> Self::Builder { + ErrorBoundaryProps::builder() + } + unsafe fn memoize(&self, _: &Self) -> bool { + false + } +} +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, non_snake_case)] +pub trait ErrorBoundaryPropsBuilder_Optional { + fn into_value T>(self, default: F) -> T; +} +impl ErrorBoundaryPropsBuilder_Optional for () { + fn into_value T>(self, default: F) -> T { + default() + } +} +impl ErrorBoundaryPropsBuilder_Optional for (T,) { + fn into_value T>(self, _: F) -> T { + self.0 + } +} +#[allow(dead_code, non_camel_case_types, missing_docs)] +impl<'a, __handle_error> ErrorBoundaryPropsBuilder<'a, ((), __handle_error)> { + pub fn children( + self, + children: Element<'a>, + ) -> ErrorBoundaryPropsBuilder<'a, ((Element<'a>,), __handle_error)> { + let children = (children,); + let (_, handle_error) = self.fields; + ErrorBoundaryPropsBuilder { + fields: (children, handle_error), + _phantom: self._phantom, + } + } +} +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, non_snake_case)] +pub enum ErrorBoundaryPropsBuilder_Error_Repeated_field_children {} +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, missing_docs)] +impl<'a, __handle_error> ErrorBoundaryPropsBuilder<'a, ((Element<'a>,), __handle_error)> { + #[deprecated(note = "Repeated field children")] + pub fn children( + self, + _: ErrorBoundaryPropsBuilder_Error_Repeated_field_children, + ) -> ErrorBoundaryPropsBuilder<'a, ((Element<'a>,), __handle_error)> { + self + } +} +#[allow(dead_code, non_camel_case_types, missing_docs)] +impl<'a, __children> ErrorBoundaryPropsBuilder<'a, (__children, ())> { + pub fn handle_error( + self, + handle_error: impl ::core::convert::Into>, + ) -> ErrorBoundaryPropsBuilder<'a, (__children, (ErrorHandler<'a>,))> { + let handle_error = (handle_error.into(),); + let (children, _) = self.fields; + ErrorBoundaryPropsBuilder { + fields: (children, handle_error), + _phantom: self._phantom, + } + } +} +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, non_snake_case)] +pub enum ErrorBoundaryPropsBuilder_Error_Repeated_field_handle_error {} +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, missing_docs)] +impl<'a, __children> ErrorBoundaryPropsBuilder<'a, (__children, (ErrorHandler<'a>,))> { + #[deprecated(note = "Repeated field handle_error")] + pub fn handle_error( + self, + _: ErrorBoundaryPropsBuilder_Error_Repeated_field_handle_error, + ) -> ErrorBoundaryPropsBuilder<'a, (__children, (ErrorHandler<'a>,))> { + self + } +} +#[allow(dead_code, non_camel_case_types, missing_docs)] +impl< + 'a, + __handle_error: ErrorBoundaryPropsBuilder_Optional>, + __children: ErrorBoundaryPropsBuilder_Optional>, + > ErrorBoundaryPropsBuilder<'a, (__children, __handle_error)> +{ + pub fn build(self) -> ErrorBoundaryProps<'a> { + let (children, handle_error) = self.fields; + let children = ErrorBoundaryPropsBuilder_Optional::into_value(children, || { + ::core::default::Default::default() + }); + let handle_error = ErrorBoundaryPropsBuilder_Optional::into_value(handle_error, || { + ErrorHandler(Box::new(default_handler)) + }); + ErrorBoundaryProps { + children, + handle_error, + } + } +} +/// Create a new error boundary component. +/// +/// ## Details +/// +/// Error boundaries handle errors within a specific part of your application. Any errors passed in a child with [`Throw`] will be caught by the nearest error boundary. +/// +/// ## Example +/// +/// ```rust, ignore +/// rsx!{ +/// ErrorBoundary { +/// handle_error: |error| rsx! { "Oops, we encountered an error. Please report {error} to the developer of this application" } +/// ThrowsError {} +/// } +/// } +/// ``` +/// +/// ## Usage +/// +/// Error boundaries are an easy way to handle errors in your application. +/// They are similar to `try/catch` in JavaScript, but they only catch errors in the tree below them. +/// Error boundaries are quick to implement, but it can be useful to individually handle errors in your components to provide a better user experience when you know that an error is likely to occur. +#[allow(non_upper_case_globals, non_snake_case)] +pub fn ErrorBoundary<'a>(cx: Scope<'a, ErrorBoundaryProps<'a>>) -> Element { + let error_boundary = use_error_boundary(cx); + match error_boundary.take_error() { + Some(error) => cx.render((cx.props.handle_error.0)(error)), + None => Some({ + let __cx = cx; + static TEMPLATE: Template = Template { + name: "examples/error_handle.rs:81:17:2342", + roots: &[TemplateNode::Dynamic { id: 0usize }], + node_paths: &[&[0u8]], + attr_paths: &[], + }; + VNode { + parent: None, + key: None, + template: std::cell::Cell::new(TEMPLATE), + root_ids: bumpalo::collections::Vec::with_capacity_in(1usize, __cx.bump()).into(), + dynamic_nodes: __cx.bump().alloc([{ + let ___nodes = (&cx.props.children).into_vnode(__cx); + ___nodes + }]), + dynamic_attrs: __cx.bump().alloc([]), + } + }), } } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 392774767..13d55826b 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -90,10 +90,11 @@ pub mod prelude { pub use crate::innerlude::{ consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context, provide_context, provide_context_to_scope, provide_root_context, push_future, - remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw, AnyValue, - Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, IntoDynNode, - LazyNodes, Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId, - Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom, + remove_future, schedule_update_any, spawn, spawn_forever, suspend, use_error_boundary, + AnyValue, Component, Element, ErrorBoundary, Event, EventHandler, Fragment, + IntoAttributeValue, IntoDynNode, LazyNodes, Properties, Runtime, RuntimeGuard, Scope, + ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, Throw, + VNode, VirtualDom, }; } diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index eeede6a81..e4cb256d2 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -1,5 +1,5 @@ use crate::{ - innerlude::{ErrorBoundary, Scheduler, SchedulerMsg}, + innerlude::{Scheduler, SchedulerMsg}, runtime::{with_current_scope, with_runtime}, Element, ScopeId, TaskId, }; @@ -7,7 +7,6 @@ use rustc_hash::FxHashSet; use std::{ any::Any, cell::{Cell, RefCell}, - fmt::Debug, future::Future, rc::Rc, sync::Arc, @@ -240,19 +239,6 @@ impl ScopeContext { self.tasks.remove(id); } - /// Inject an error into the nearest error boundary and quit rendering - /// - /// The error doesn't need to implement Error or any specific traits since the boundary - /// itself will downcast the error into a trait object. - pub fn throw(&self, error: impl Debug + 'static) -> Option<()> { - if let Some(cx) = self.consume_context::>() { - cx.insert_error(self.scope_id(), Box::new(error)); - } - - // Always return none during a throw - None - } - /// Mark this component as suspended and then return None pub fn suspend(&self) -> Option { self.suspended.set(true); @@ -322,11 +308,6 @@ pub fn suspend() -> Option> { None } -/// Throw an error into the nearest error boundary -pub fn throw(error: impl Debug + 'static) -> Option<()> { - with_current_scope(|cx| cx.throw(error)).flatten() -} - /// Pushes the future onto the poll queue to be polled after the component renders. pub fn push_future(fut: impl Future + 'static) -> Option { with_current_scope(|cx| cx.push_future(fut)) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 6cb88604d..1a26d3847 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -518,19 +518,6 @@ impl<'src> ScopeState { AttributeValue::Any(RefCell::new(Some(boxed))) } - /// Inject an error into the nearest error boundary and quit rendering - /// - /// The error doesn't need to implement Error or any specific traits since the boundary - /// itself will downcast the error into a trait object. - pub fn throw(&self, error: impl Debug + 'static) -> Option<()> { - if let Some(cx) = self.consume_context::>() { - cx.insert_error(self.scope_id(), Box::new(error)); - } - - // Always return none during a throw - None - } - /// Mark this component as suspended and then return None pub fn suspend(&self) -> Option { let cx = self.context(); diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index d1951b388..f469cd9e2 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -16,7 +16,7 @@ use crate::{ use futures_util::{pin_mut, StreamExt}; use rustc_hash::{FxHashMap, FxHashSet}; use slab::Slab; -use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, ptr::NonNull, rc::Rc}; +use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, ptr::NonNull, rc::Rc, sync::Arc}; /// A virtual node system that progresses user events and diffs UI trees. /// @@ -277,7 +277,10 @@ impl VirtualDom { ); // Unlike react, we provide a default error boundary that just renders the error as a string - root.provide_context(Rc::new(ErrorBoundary::new(ScopeId::ROOT))); + root.provide_context(Rc::new(ErrorBoundary::new_in_scope( + ScopeId::ROOT, + Arc::new(|_| {}), + ))); // the root element is always given element ID 0 since it's the container for the entire tree dom.elements.insert(None); diff --git a/packages/core/tests/error_boundary.rs b/packages/core/tests/error_boundary.rs index ce26fba31..58d861169 100644 --- a/packages/core/tests/error_boundary.rs +++ b/packages/core/tests/error_boundary.rs @@ -24,9 +24,9 @@ fn NoneChild(_cx: Scope) -> Element { } fn ThrowChild(cx: Scope) -> Element { - cx.throw(std::io::Error::new(std::io::ErrorKind::AddrInUse, "asd"))?; + Err(std::io::Error::new(std::io::ErrorKind::AddrInUse, "asd")).throw()?; - let _g: i32 = "123123".parse().throw(cx)?; + let _g: i32 = "123123".parse().throw()?; cx.render(rsx! { div {}