leptos-rs/leptos · error

body to exist

Error message

body to exist

What it means

The <Portal/> component mounts its children into a mount target element; when none is given it falls back to document().body().expect("body to exist"). The panic means document.body was null when the Portal was created — the code is running in a DOM-less environment (SSR, or a script executed before <body> is parsed).

Source

Thrown at leptos/src/portal.rs:40

    /// When using SVG this has to be set to `true`. Defaults to `false`.
    #[prop(optional)]
    is_svg: bool,
    /// The children to teleport into the `mount` element
    children: TypedChildrenFn<V>,
) -> impl IntoView
where
    V: IntoView + 'static,
{
    if cfg!(target_arch = "wasm32")
        && Owner::current_shared_context()
            .map(|sc| sc.is_browser())
            .unwrap_or(true)
    {
        use send_wrapper::SendWrapper;
        use wasm_bindgen::JsCast;

        let mount = mount.unwrap_or_else(|| {
            document().body().expect("body to exist").unchecked_into()
        });
        let children = children.into_inner();

        Effect::new(move |_| {
            let container = if is_svg {
                document()
                    .create_element_ns(Some("http://www.w3.org/2000/svg"), "g")
                    .expect("SVG element creation to work")
            } else {
                document()
                    .create_element("div")
                    .expect("HTML element creation to work")
            };

            let render_root = if use_shadow {
                container
                    .attach_shadow(&web_sys::ShadowRootInit::new(
                        web_sys::ShadowRootMode::Open,

View on GitHub (pinned to 32d20f6c9d)

Solutions

  1. Pass an explicit mount element to <Portal mount={...}/> so body is never needed.
  2. Only render <Portal/> on the client, e.g. inside a component gated on a mounted signal or hydration completion.
  3. Load your wasm script at the end of <body> or with defer so body exists.
  4. In tests, ensure the fixture creates and attaches document.body.

Example fix

// before
view! { <Portal><div/>"></Portal> } // SSR: body is None
// after
let mount = document().get_element_by_id("app");
view! { <Portal mount={mount}><div/>"></Portal> } // or gate on is_client
Defensive patterns

Strategy: validation

Validate before calling

if document().body().is_none() {
    // skip Portal or wait for client mount
    return;
}

Type guard

fn body_ready() -> bool { document().body().is_some() }

Prevention

When it happens

Trigger: Rendering <Portal/> without a mount prop during SSR; running client-side code from <head> before <body> exists; executing in a jsdom/test environment with no body attached.

Common situations: SSR hydration pass accidentally rendering Portal; scripts loaded with default (blocking) placement in <head>; wasm tests that never append a body to the document.

Related errors


AI-assisted analysis of leptos-rs/leptos@32d20f6c9d (2026-09-01). Data as JSON: /api/errors/2a10e0933852a39b. Report an issue: GitHub.