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(
¤t_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
- Make route loaders resolve synchronously on the server: load route definitions eagerly and fetch data via blocking `<Suspense>`/resources instead.
- Gate lazy loading behind client-only checks so the server always uses the already-available route/view.
- 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
- Register routes eagerly in server builds; reserve lazy loading for the client.
- Use blocking resources for data fetching in SSR.
- Cover SSR rendering in tests to surface async loaders early.
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
- async route used in SSR
- body to exist
- no shared context
- children should not be removed until we render here
- you are using leptos_meta without a </head> tag
AI-assisted analysis of leptos-rs/leptos@32d20f6c9d (2026-09-01).
Data as JSON: /api/errors/1821cb104b4175de.
Report an issue: GitHub.