DioxusLabs / dioxus

Fullstack GUI library for web, desktop, mobile, and more.
https://dioxuslabs.com
Apache License 2.0
19.38k stars 747 forks source link

Errors don't bubble through a SuspenseBoundary #2570

Open DonAlonzo opened 6 days ago

DonAlonzo commented 6 days ago

Problem

The following code produces an unwrap error:

use serde::{Deserialize, Serialize};
use strum::{AsRefStr, Display, EnumString};

use crate::prelude::*;

#[derive(
    Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize, AsRefStr, Display, EnumString,
)]
#[strum(serialize_all = "snake_case")]
pub enum ServerError {
    Unauthenticated,
}

impl std::error::Error for ServerError {}

#[component]
pub fn App() -> Element {
    rsx! {
        ErrorBoundary {
            handle_error: |_| rsx! {
                "Hmm, something went wrong."
            },
            Home {}
        }
    }
}

#[component]
pub fn Home() -> Element {
    rsx! {
        SuspenseBoundary {
            fallback: |context: SuspenseContext| rsx! {
                div {
                    "Loading..."
                }
            },
            Stuff {
                id: "floob_123",
            }
        }
    }
}

#[component]
pub fn Stuff(id: String) -> Element {
    let Some(floob) = use_floob(id)?.suspend()?()? else {
        return rsx! {
            "No floob"
        };
    };
    rsx! {
        {floob}
    }
}

#[server]
pub async fn floob(id: String) -> Result<Option<String>, ServerFnError<ServerError>> {
    tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
    Err(ServerFnError::WrappedServerError(ServerError::Unauthenticated))
}

pub fn use_floob(id: String) -> Result<Resource<Result<Option<String>, ServerFnError<ServerError>>>, RenderError> {
    use_server_future(move || {
        to_owned![id];
        async move {
            floob(id).await
        }
    })
}

Running it gives the following error: image

Changing Home to this code, however, works:

#[component]
pub fn Home() -> Element {
    rsx! {
        SuspenseBoundary {
            fallback: |context: SuspenseContext| rsx! {
                div {
                    "Loading..."
                }
            },
            ErrorBoundary {
                handle_error: |_| rsx! {
                    "Hmm, something went wrong inside of the suspense boundary."
                },
                Stuff {
                    id: "floob_123",
                }
            }
        }
    }
}

The error can't bubble through the SuspenseBoundary correctly.

Environment:

Questionnaire

ealmloff commented 5 days ago

2575 improves the error message for this situation, but we still need better docs/warnings around how suspense boundaries and error boundaries interact.

During suspense, the server gradually hands off control for part of the tree to the client. Once part of the tree is resolved, that part of the tree can no longer be updated because it will either lose client state or cause a hydration mismatch. In the code you posted, the suspense boundary throws an error up into an error boundary that is already resolved. Swapping the suspense and error boundaries fixes the issue because the error boundary is moved into a part of the tree that is not resolved. Then when the suspense throws an error, it updates the unresolved part of the tree on the server and resolves suspense