leptos-rs/leptos · critical

async routes not supported in SSR

Error message

async routes not supported in SSR

What it means

In nested_router.rs `to_html_with_buf`, route loaders are polled with `join_all(...).now_or_never()`; loaders are async only to enable lazy-loading in the browser, but on the server they must resolve synchronously. If any loader awaits (e.g. lazy/dynamic route loading), the future is Pending, `now_or_never` returns None, and this panic fires.

Source

Thrown at router/src/nested_router.rs:383

            let new_match = routes.match_route(current_url.path());
            let view = match new_match {
                None => Either::Left(fallback()),
                Some(route) => {
                    let mut loaders = Vec::new();
                    route.build_nested_route(
                        &current_url,
                        base,
                        &mut loaders,
                        &mut outlets,
                        &outer_owner,
                    );

                    // outlets will not send their views if the loaders are never polled
                    // the loaders are async so that they can lazy-load routes in the browser,
                    // but they should always be synchronously available on the server
                    join_all(mem::take(&mut loaders))
                        .now_or_never()
                        .expect("async routes not supported in SSR");

                    Either::Right(top_level_outlet(&outlets, &outer_owner))
                }
            };
            view.to_html_with_buf(
                buf,
                position,
                escape,
                mark_branches,
                extra_attrs,
            );
        }
    }

    fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
        self,
        buf: &mut StreamBuilder,
        position: &mut Position,

View on GitHub (pinned to 32d20f6c9d)

Solutions

  1. Make route loaders resolve synchronously on the server: load route definitions eagerly and fetch data via blocking `<Suspense>`/resources instead.
  2. Gate lazy loading behind client-only checks so the server always uses the already-available route/view.
  3. Use `to_html_async_with_buf` (the async variant) if the surrounding server stack supports async rendering, though loaders still should be sync per router contract.

Example fix

// before
let routes = lazy_routes(); // loader awaits dynamic import -> panics in SSR

// after
let routes = if is_server() { eager_routes() } else { lazy_routes() };
Defensive patterns

Strategy: validation

Validate before calling

// SSR pre-check: loaders must be sync on the server
if is_server() && loaders.iter().any(|l| !l.is_ready()) {
    panic!("route loader is async in SSR");
}

Type guard

fn loader_ssr_safe(l: &Loader) -> bool { !l.is_async() || is_browser() }

Prevention

When it happens

Trigger: Server-rendering (`to_html_with_buf`) with a route whose loader is genuinely async (lazy route module loading, async data fetch in the loader), so it doesn't complete on first poll.

Common situations: Using browser-side lazy route loading in an SSR app; defining async route components where blocking resources should be used; porting a client-only app to SSR without making loaders sync on the server.

Related errors


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