Skip to main content

leptos/
error_boundary.rs

1use crate::{children::TypedChildren, IntoView};
2use futures::{channel::oneshot, future::join_all};
3use hydration_context::{SerializedDataId, SharedContext};
4use leptos_macro::component;
5use or_poisoned::OrPoisoned;
6use reactive_graph::{
7    computed::ArcMemo,
8    effect::RenderEffect,
9    owner::{provide_context, ArcStoredValue, Owner},
10    signal::ArcRwSignal,
11    traits::{Get, Update, With, WithUntracked, WriteValue},
12};
13use rustc_hash::FxHashMap;
14use std::{
15    collections::VecDeque,
16    fmt::Debug,
17    mem,
18    sync::{Arc, Mutex},
19};
20use tachys::{
21    html::attribute::{any_attribute::AnyAttribute, Attribute},
22    hydration::Cursor,
23    reactive_graph::OwnedView,
24    ssr::{StreamBuilder, StreamChunk},
25    view::{
26        add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
27        RenderHtml,
28    },
29};
30use throw_error::{Error, ErrorHook, ErrorId};
31
32/// When you render a `Result<_, _>` in your view, in the `Err` case it will
33/// render nothing, and search up through the view tree for an `<ErrorBoundary/>`.
34/// This component lets you define a fallback that should be rendered in that
35/// error case, allowing you to handle errors within a section of the interface.
36///
37/// ```
38/// # use leptos::prelude::*;
39/// #[component]
40/// pub fn ErrorBoundaryExample() -> impl IntoView {
41///   let (value, set_value) = signal(Ok(0));
42///   let on_input =
43///     move |ev| set_value.set(event_target_value(&ev).parse::<i32>());
44///
45///   view! {
46///     <input type="text" on:input=on_input/>
47///     <ErrorBoundary
48///       fallback=move |_| view! { <p class="error">"Enter a valid number."</p>}
49///     >
50///       <p>"Value is: " {move || value.get()}</p>
51///     </ErrorBoundary>
52///   }
53/// }
54/// ```
55///
56/// ## Beginner's Tip: ErrorBoundary Requires Your Error To Implement std::error::Error.
57/// `ErrorBoundary` requires your `Result<T,E>` to implement [IntoView](https://docs.rs/leptos/latest/leptos/trait.IntoView.html).
58/// `Result<T,E>` only implements `IntoView` if `E` implements [std::error::Error](https://doc.rust-lang.org/std/error/trait.Error.html).
59/// So, for instance, if you pass a `Result<T,String>` where `T` implements [IntoView](https://docs.rs/leptos/latest/leptos/trait.IntoView.html)
60/// and attempt to render the error for the purposes of `ErrorBoundary` you'll get a compiler error like this.
61///
62/// ```rust,ignore
63/// error[E0599]: the method `into_view` exists for enum `Result<ViewableLoginFlow, String>`, but its trait bounds were not satisfied
64///    --> src/login.rs:229:32
65///     |
66/// 229 |                     err => err.into_view(),
67///     |                                ^^^^^^^^^ method cannot be called on `Result<ViewableLoginFlow, String>` due to unsatisfied trait bounds
68///     |
69///     = note: the following trait bounds were not satisfied:
70///             `<&Result<ViewableLoginFlow, std::string::String> as FnOnce<()>>::Output = _`
71///             which is required by `&Result<ViewableLoginFlow, std::string::String>: leptos::IntoView`
72///    ... more notes here ...
73/// ```
74///
75/// For more information about how to easily implement `Error` see
76/// [thiserror](https://docs.rs/thiserror/latest/thiserror/)
77#[component]
78pub fn ErrorBoundary<FalFn, Fal, Chil>(
79    /// The elements that will be rendered, which may include one or more `Result<_>` types.
80    children: TypedChildren<Chil>,
81    /// A fallback that will be shown if an error occurs.
82    fallback: FalFn,
83) -> impl IntoView
84where
85    FalFn: FnMut(ArcRwSignal<Errors>) -> Fal + Send + 'static,
86    Fal: IntoView + Send + 'static,
87    Chil: IntoView + Send + 'static,
88{
89    let sc = Owner::current_shared_context();
90    let boundary_id = sc.as_ref().map(|sc| sc.next_id()).unwrap_or_default();
91    let initial_errors =
92        sc.map(|sc| sc.errors(&boundary_id)).unwrap_or_default();
93
94    let hook = Arc::new(ErrorBoundaryErrorHook::new(
95        boundary_id.clone(),
96        initial_errors,
97    ));
98    let errors = hook.errors.clone();
99    let errors_empty = ArcMemo::new({
100        let errors = errors.clone();
101        move |_| errors.with(|map| map.is_empty())
102    });
103    let hook = hook as Arc<dyn ErrorHook>;
104
105    let _guard = throw_error::set_error_hook(Arc::clone(&hook));
106    let suspended_children = ErrorBoundarySuspendedChildren::default();
107
108    let owner = Owner::new();
109    let children = owner.with(|| {
110        provide_context(Arc::clone(&hook));
111        provide_context(suspended_children.clone());
112        children.into_inner()()
113    });
114
115    OwnedView::new_with_owner(
116        ErrorBoundaryView {
117            hook,
118            boundary_id,
119            errors_empty,
120            children,
121            errors,
122            fallback,
123            suspended_children,
124        },
125        owner,
126    )
127}
128
129pub(crate) type ErrorBoundarySuspendedChildren =
130    ArcStoredValue<Vec<oneshot::Receiver<()>>>;
131
132struct ErrorBoundaryView<Chil, FalFn> {
133    hook: Arc<dyn ErrorHook>,
134    boundary_id: SerializedDataId,
135    errors_empty: ArcMemo<bool>,
136    children: Chil,
137    fallback: FalFn,
138    errors: ArcRwSignal<Errors>,
139    suspended_children: ErrorBoundarySuspendedChildren,
140}
141
142struct ErrorBoundaryViewState<Chil, Fal> {
143    // the children are always present; we toggle between them and the fallback as needed
144    children: Chil,
145    fallback: Option<Fal>,
146}
147
148impl<Chil, Fal> Mountable for ErrorBoundaryViewState<Chil, Fal>
149where
150    Chil: Mountable,
151    Fal: Mountable,
152{
153    fn unmount(&mut self) {
154        if let Some(fallback) = &mut self.fallback {
155            fallback.unmount();
156        } else {
157            self.children.unmount();
158        }
159    }
160
161    fn mount(
162        &mut self,
163        parent: &tachys::renderer::types::Element,
164        marker: Option<&tachys::renderer::types::Node>,
165    ) {
166        if let Some(fallback) = &mut self.fallback {
167            fallback.mount(parent, marker);
168        } else {
169            self.children.mount(parent, marker);
170        }
171    }
172
173    fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
174        if let Some(fallback) = &self.fallback {
175            fallback.insert_before_this(child)
176        } else {
177            self.children.insert_before_this(child)
178        }
179    }
180
181    fn elements(&self) -> Vec<tachys::renderer::types::Element> {
182        if let Some(fallback) = &self.fallback {
183            fallback.elements()
184        } else {
185            self.children.elements()
186        }
187    }
188}
189
190impl<Chil, FalFn, Fal> Render for ErrorBoundaryView<Chil, FalFn>
191where
192    Chil: Render + 'static,
193    FalFn: FnMut(ArcRwSignal<Errors>) -> Fal + Send + 'static,
194    Fal: Render + 'static,
195{
196    type State = RenderEffect<ErrorBoundaryViewState<Chil::State, Fal::State>>;
197
198    fn build(mut self) -> Self::State {
199        let hook = Arc::clone(&self.hook);
200        let _hook = throw_error::set_error_hook(Arc::clone(&hook));
201        let mut children = Some(self.children.build());
202        RenderEffect::new(
203            move |prev: Option<
204                ErrorBoundaryViewState<Chil::State, Fal::State>,
205            >| {
206                let _hook = throw_error::set_error_hook(Arc::clone(&hook));
207                if let Some(mut state) = prev {
208                    match (self.errors_empty.get(), &mut state.fallback) {
209                        // no errors, and was showing fallback
210                        (true, Some(fallback)) => {
211                            fallback.insert_before_this(&mut state.children);
212                            fallback.unmount();
213                            state.fallback = None;
214                        }
215                        // yes errors, and was showing children
216                        (false, None) => {
217                            state.fallback = Some(
218                                (self.fallback)(self.errors.clone()).build(),
219                            );
220                            state
221                                .children
222                                .insert_before_this(&mut state.fallback);
223                            state.children.unmount();
224                        }
225                        // either there were no errors, and we were already showing the children
226                        // or there are errors, but we were already showing the fallback
227                        // in either case, rebuilding doesn't require us to do anything
228                        _ => {}
229                    }
230                    state
231                } else {
232                    let fallback = (!self.errors_empty.get())
233                        .then(|| (self.fallback)(self.errors.clone()).build());
234                    ErrorBoundaryViewState {
235                        children: children.take().unwrap(),
236                        fallback,
237                    }
238                }
239            },
240        )
241    }
242
243    fn rebuild(self, state: &mut Self::State) {
244        let new = self.build();
245        let mut old = std::mem::replace(state, new);
246        old.insert_before_this(state);
247        old.unmount();
248    }
249}
250
251impl<Chil, FalFn, Fal> AddAnyAttr for ErrorBoundaryView<Chil, FalFn>
252where
253    Chil: RenderHtml + 'static,
254    FalFn: FnMut(ArcRwSignal<Errors>) -> Fal + Send + 'static,
255    Fal: RenderHtml + Send + 'static,
256{
257    type Output<SomeNewAttr: Attribute> =
258        ErrorBoundaryView<Chil::Output<SomeNewAttr::CloneableOwned>, FalFn>;
259
260    fn add_any_attr<NewAttr: Attribute>(
261        self,
262        attr: NewAttr,
263    ) -> Self::Output<NewAttr>
264    where
265        Self::Output<NewAttr>: RenderHtml,
266    {
267        let ErrorBoundaryView {
268            hook,
269            boundary_id,
270            errors_empty,
271            children,
272            fallback,
273            errors,
274            suspended_children,
275        } = self;
276        ErrorBoundaryView {
277            hook,
278            boundary_id,
279            errors_empty,
280            children: children.add_any_attr(attr.into_cloneable_owned()),
281            fallback,
282            errors,
283            suspended_children,
284        }
285    }
286}
287
288impl<Chil, FalFn, Fal> RenderHtml for ErrorBoundaryView<Chil, FalFn>
289where
290    Chil: RenderHtml + Send + 'static,
291    FalFn: FnMut(ArcRwSignal<Errors>) -> Fal + Send + 'static,
292    Fal: RenderHtml + Send + 'static,
293{
294    type AsyncOutput = ErrorBoundaryView<Chil::AsyncOutput, FalFn>;
295    type Owned = Self;
296
297    const MIN_LENGTH: usize = Chil::MIN_LENGTH;
298
299    fn dry_resolve(&mut self) {
300        self.children.dry_resolve();
301    }
302
303    async fn resolve(self) -> Self::AsyncOutput {
304        let ErrorBoundaryView {
305            hook,
306            boundary_id,
307            errors_empty,
308            children,
309            fallback,
310            errors,
311            suspended_children,
312            ..
313        } = self;
314        ErrorBoundaryView {
315            hook,
316            boundary_id,
317            errors_empty,
318            children: children.resolve().await,
319            fallback,
320            errors,
321            suspended_children,
322        }
323    }
324
325    fn to_html_with_buf(
326        mut self,
327        buf: &mut String,
328        position: &mut Position,
329        escape: bool,
330        mark_branches: bool,
331        extra_attrs: Vec<AnyAttribute>,
332    ) {
333        // first, attempt to serialize the children to HTML, then check for errors
334        let _hook = throw_error::set_error_hook(self.hook);
335        let mut new_buf = String::with_capacity(Chil::MIN_LENGTH);
336        let mut new_pos = *position;
337        self.children.to_html_with_buf(
338            &mut new_buf,
339            &mut new_pos,
340            escape,
341            mark_branches,
342            extra_attrs.clone(),
343        );
344
345        // any thrown errors would've been caught here
346        if self.errors.with_untracked(|map| map.is_empty()) {
347            buf.push_str(&new_buf);
348        } else {
349            // otherwise, serialize the fallback instead
350            (self.fallback)(self.errors).to_html_with_buf(
351                buf,
352                position,
353                escape,
354                mark_branches,
355                extra_attrs,
356            );
357        }
358    }
359
360    fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
361        mut self,
362        buf: &mut StreamBuilder,
363        position: &mut Position,
364        escape: bool,
365        mark_branches: bool,
366        extra_attrs: Vec<AnyAttribute>,
367    ) where
368        Self: Sized,
369    {
370        let _hook = throw_error::set_error_hook(Arc::clone(&self.hook));
371
372        // first, attempt to serialize the children to HTML, then check for errors
373        let mut new_buf = StreamBuilder::new(buf.clone_id());
374        let mut new_pos = *position;
375        self.children.to_html_async_with_buf::<OUT_OF_ORDER>(
376            &mut new_buf,
377            &mut new_pos,
378            escape,
379            mark_branches,
380            extra_attrs.clone(),
381        );
382
383        let suspense_children =
384            mem::take(&mut *self.suspended_children.write_value());
385
386        // not waiting for any suspended children: just render
387        if suspense_children.is_empty() {
388            // any thrown errors would've been caught here
389            if self.errors.with_untracked(|map| map.is_empty()) {
390                buf.append(new_buf);
391            } else {
392                // otherwise, serialize the fallback instead
393                let mut fallback = String::with_capacity(Fal::MIN_LENGTH);
394                (self.fallback)(self.errors).to_html_with_buf(
395                    &mut fallback,
396                    position,
397                    escape,
398                    mark_branches,
399                    extra_attrs,
400                );
401                buf.push_sync(&fallback);
402            }
403        } else {
404            let mut position = *position;
405            // if we're waiting for suspended children, we'll first wait for them to load
406            // in this implementation, an ErrorBoundary that *contains* Suspense essentially acts
407            // like a Suspense: it will wait for (all top-level) child Suspense to load before rendering anything
408            let mut view_buf = StreamBuilder::new(new_buf.clone_id());
409            view_buf.next_id();
410            let hook = Arc::clone(&self.hook);
411            view_buf.push_async(async move {
412                let _hook = throw_error::set_error_hook(Arc::clone(&hook));
413                let _ = join_all(suspense_children).await;
414
415                let mut my_chunks = VecDeque::new();
416                for chunk in new_buf.take_chunks() {
417                    match chunk {
418                        StreamChunk::Sync(data) => {
419                            my_chunks.push_back(StreamChunk::Sync(data))
420                        }
421                        StreamChunk::Async { chunks } => {
422                            let chunks = chunks.await;
423                            my_chunks.extend(chunks);
424                        }
425                        StreamChunk::OutOfOrder { chunks } => {
426                            let chunks = chunks.await;
427                            my_chunks.push_back(StreamChunk::OutOfOrder {
428                                chunks: Box::pin(async move { chunks }),
429                            });
430                        }
431                    }
432                }
433
434                if self.errors.with_untracked(|map| map.is_empty()) {
435                    // if no errors, just go ahead with the stream
436                    my_chunks
437                } else {
438                    // otherwise, serialize the fallback instead
439                    let mut fallback = String::with_capacity(Fal::MIN_LENGTH);
440                    (self.fallback)(self.errors).to_html_with_buf(
441                        &mut fallback,
442                        &mut position,
443                        escape,
444                        mark_branches,
445                        extra_attrs,
446                    );
447                    my_chunks.clear();
448                    my_chunks.push_back(StreamChunk::Sync(fallback));
449                    my_chunks
450                }
451            });
452            buf.append(view_buf);
453        }
454    }
455
456    fn hydrate<const FROM_SERVER: bool>(
457        mut self,
458        cursor: &Cursor,
459        position: &PositionState,
460    ) -> Self::State {
461        let mut children = Some(self.children);
462        let hook = Arc::clone(&self.hook);
463        let cursor = cursor.to_owned();
464        let position = position.to_owned();
465        RenderEffect::new(
466            move |prev: Option<
467                ErrorBoundaryViewState<Chil::State, Fal::State>,
468            >| {
469                let _hook = throw_error::set_error_hook(Arc::clone(&hook));
470                if let Some(mut state) = prev {
471                    match (self.errors_empty.get(), &mut state.fallback) {
472                        // no errors, and was showing fallback
473                        (true, Some(fallback)) => {
474                            fallback.insert_before_this(&mut state.children);
475                            state.fallback.unmount();
476                            state.fallback = None;
477                        }
478                        // yes errors, and was showing children
479                        (false, None) => {
480                            state.fallback = Some(
481                                (self.fallback)(self.errors.clone()).build(),
482                            );
483                            state
484                                .children
485                                .insert_before_this(&mut state.fallback);
486                            state.children.unmount();
487                        }
488                        // either there were no errors, and we were already showing the children
489                        // or there are errors, but we were already showing the fallback
490                        // in either case, rebuilding doesn't require us to do anything
491                        _ => {}
492                    }
493                    state
494                } else {
495                    let children = children.take().unwrap();
496                    let (children, fallback) = if self.errors_empty.get() {
497                        (
498                            children.hydrate::<FROM_SERVER>(&cursor, &position),
499                            None,
500                        )
501                    } else {
502                        (
503                            children.build(),
504                            Some(
505                                (self.fallback)(self.errors.clone())
506                                    .hydrate::<FROM_SERVER>(&cursor, &position),
507                            ),
508                        )
509                    };
510
511                    ErrorBoundaryViewState { children, fallback }
512                }
513            },
514        )
515    }
516
517    async fn hydrate_async(
518        self,
519        cursor: &Cursor,
520        position: &PositionState,
521    ) -> Self::State {
522        let mut children = Some(self.children);
523        let hook = Arc::clone(&self.hook);
524        let cursor = cursor.to_owned();
525        let position = position.to_owned();
526
527        let fallback_fn = Arc::new(Mutex::new(self.fallback));
528        let initial = {
529            let errors_empty = self.errors_empty.clone();
530            let errors = self.errors.clone();
531            let fallback_fn = Arc::clone(&fallback_fn);
532            async move {
533                let children = children.take().unwrap();
534                let (children, fallback) = if errors_empty.get() {
535                    (children.hydrate_async(&cursor, &position).await, None)
536                } else {
537                    let children = children.build();
538                    let fallback =
539                        (fallback_fn.lock().or_poisoned())(errors.clone());
540                    let fallback =
541                        fallback.hydrate_async(&cursor, &position).await;
542                    (children, Some(fallback))
543                };
544
545                ErrorBoundaryViewState { children, fallback }
546            }
547        };
548
549        RenderEffect::new_with_async_value(
550            move |prev: Option<
551                ErrorBoundaryViewState<Chil::State, Fal::State>,
552            >| {
553                let _hook = throw_error::set_error_hook(Arc::clone(&hook));
554                if let Some(mut state) = prev {
555                    match (self.errors_empty.get(), &mut state.fallback) {
556                        // no errors, and was showing fallback
557                        (true, Some(fallback)) => {
558                            fallback.insert_before_this(&mut state.children);
559                            state.fallback.unmount();
560                            state.fallback = None;
561                        }
562                        // yes errors, and was showing children
563                        (false, None) => {
564                            state.fallback = Some(
565                                (fallback_fn.lock().or_poisoned())(
566                                    self.errors.clone(),
567                                )
568                                .build(),
569                            );
570                            state
571                                .children
572                                .insert_before_this(&mut state.fallback);
573                            state.children.unmount();
574                        }
575                        // either there were no errors, and we were already showing the children
576                        // or there are errors, but we were already showing the fallback
577                        // in either case, rebuilding doesn't require us to do anything
578                        _ => {}
579                    }
580                    state
581                } else {
582                    unreachable!()
583                }
584            },
585            initial,
586        )
587        .await
588    }
589
590    fn into_owned(self) -> Self::Owned {
591        self
592    }
593}
594
595#[derive(Debug)]
596struct ErrorBoundaryErrorHook {
597    errors: ArcRwSignal<Errors>,
598    id: SerializedDataId,
599    shared_context: Option<Arc<dyn SharedContext + Send + Sync>>,
600}
601
602impl ErrorBoundaryErrorHook {
603    pub fn new(
604        id: SerializedDataId,
605        initial_errors: impl IntoIterator<Item = (ErrorId, Error)>,
606    ) -> Self {
607        Self {
608            errors: ArcRwSignal::new(Errors(
609                initial_errors.into_iter().collect(),
610            )),
611            id,
612            shared_context: Owner::current_shared_context(),
613        }
614    }
615}
616
617impl ErrorHook for ErrorBoundaryErrorHook {
618    fn throw(&self, error: Error) -> ErrorId {
619        // generate a unique ID
620        let key: ErrorId = Owner::current_shared_context()
621            .map(|sc| sc.next_id())
622            .unwrap_or_default()
623            .into();
624
625        // register it with the shared context, so that it can be serialized from server to client
626        // as needed
627        if let Some(sc) = &self.shared_context {
628            sc.register_error(self.id.clone(), key.clone(), error.clone());
629        }
630
631        // add it to the reactive map of errors
632        self.errors.update(|map| {
633            map.insert(key.clone(), error);
634        });
635
636        // return the key, which will be owned by the Result being rendered and can be used to
637        // unregister this error if it is rebuilt
638        key
639    }
640
641    fn clear(&self, id: &throw_error::ErrorId) {
642        self.errors.update(|map| {
643            map.remove(id);
644        });
645    }
646}
647
648/// A struct to hold all the possible errors that could be provided by child Views
649#[derive(Debug, Clone, Default)]
650#[repr(transparent)]
651pub struct Errors(FxHashMap<ErrorId, Error>);
652
653impl Errors {
654    /// Returns `true` if there are no errors.
655    #[inline(always)]
656    pub fn is_empty(&self) -> bool {
657        self.0.is_empty()
658    }
659
660    /// Add an error to Errors that will be processed by `<ErrorBoundary/>`
661    pub fn insert<E>(&mut self, key: ErrorId, error: E)
662    where
663        E: Into<Error>,
664    {
665        self.0.insert(key, error.into());
666    }
667
668    /// Add an error with the default key for errors outside the reactive system
669    pub fn insert_with_default_key<E>(&mut self, error: E)
670    where
671        E: Into<Error>,
672    {
673        self.0.insert(Default::default(), error.into());
674    }
675
676    /// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
677    pub fn remove(&mut self, key: &ErrorId) -> Option<Error> {
678        self.0.remove(key)
679    }
680
681    /// An iterator over all the errors, in arbitrary order.
682    #[inline(always)]
683    pub fn iter(&self) -> Iter<'_> {
684        Iter(self.0.iter())
685    }
686}
687
688impl IntoIterator for Errors {
689    type Item = (ErrorId, Error);
690    type IntoIter = IntoIter;
691
692    #[inline(always)]
693    fn into_iter(self) -> Self::IntoIter {
694        IntoIter(self.0.into_iter())
695    }
696}
697
698/// An owning iterator over all the errors contained in the [`Errors`] struct.
699#[repr(transparent)]
700pub struct IntoIter(std::collections::hash_map::IntoIter<ErrorId, Error>);
701
702impl Iterator for IntoIter {
703    type Item = (ErrorId, Error);
704
705    #[inline(always)]
706    fn next(
707        &mut self,
708    ) -> std::option::Option<<Self as std::iter::Iterator>::Item> {
709        self.0.next()
710    }
711}
712
713/// An iterator over all the errors contained in the [`Errors`] struct.
714#[repr(transparent)]
715pub struct Iter<'a>(std::collections::hash_map::Iter<'a, ErrorId, Error>);
716
717impl<'a> Iterator for Iter<'a> {
718    type Item = (&'a ErrorId, &'a Error);
719
720    #[inline(always)]
721    fn next(
722        &mut self,
723    ) -> std::option::Option<<Self as std::iter::Iterator>::Item> {
724        self.0.next()
725    }
726}