leptos-rs/leptos · critical
async route used in SSR
Error message
async route used in SSR
What it means
In flat_router.rs, SSR rendering polls the async route view future with `.now_or_never()`, which only works if the future completes on its first poll. If the route's `choose()` awaits anything (e.g. a lazy-loaded async route component) and does not resolve immediately, the `expect` panics with "async route used in SSR". The router requires SSR route views to be synchronously resolvable; async routes are only supported in the browser where lazy loading can actually happen.
Source
Thrown at router/src/flat_router.rs:535
// release URL lock
drop(current_url);
let view = match new_match {
None => (self.fallback)().into_any(),
Some(new_match) => {
let id = new_match.as_matched().to_string();
let (view, _) = new_match.into_view_and_child();
let view = owner
.with(|| {
provide_context(url);
provide_context(params_memo);
provide_context(Matched(ArcMemo::from(matched)));
ScopedFuture::new(async move { view.choose().await })
})
.now_or_never()
.expect("async route used in SSR");
let view = MatchedRoute(id, view);
view.into_any()
}
};
OwnedView::new_with_owner(view, owner)
}
}
impl<Loc, Defs, FalFn, Fal> RenderHtml for FlatRoutesView<Loc, Defs, FalFn>
where
Loc: LocationProvider + Send,
Defs: MatchNestedRoutes + Send + 'static,
FalFn: FnOnce() -> Fal + Send + 'static,
Fal: RenderHtml + 'static,
{
type AsyncOutput = Self;
type Owned = Self;View on GitHub (pinned to 32d20f6c9d)
Solutions
- Make the route's view/component resolve synchronously on the server: avoid `.await` in the view and load data instead via `<Suspense>` with a blocking resource.
- Move lazy loading client-side only (e.g. gate dynamic loading behind `is_server()`/cfg so the server uses the already-loaded component).
- Ensure blocking resources are used for async data so routes are available before render, matching the router's requirement that loaders/views be sync on the server.
Example fix
// before
#[component]
async fn Profile() -> impl IntoView {
let data = load_profile().await;
view! { <p>{data.name}</p> }
}
// after
#[component]
fn Profile() -> impl IntoView {
let data = Resource::new(|| (), |_| load_profile());
view! {
<Suspense fallback=|| ()>
{move || Suspend::new(async move { let d = data.await; view! { <p>{d.name}</p> } })}
</Suspense>
}
} Defensive patterns
Strategy: validation
Validate before calling
// Before SSR, assert no route view awaits:
if is_server() && routes.iter().any(|r| r.is_async()) {
panic!("async route detected in SSR; use blocking Suspense instead");
} Type guard
fn is_ssr_safe(route: &RouteDef) -> bool { !route.is_async() || cfg!(feature = "ssr") == false } Prevention
- Never `.await` inside route view components; use blocking resources with <Suspense>.
- Gate lazy loading behind is_server()/cfg so server builds get eager routes.
- Test SSR rendering in CI so async-only routes fail at build/test time, not in production.
When it happens
Trigger: Rendering a route defined with an async component / `AsyncProp` / lazy-loaded `#[component(async)]`-style view through `to_html_with_buf` or `to_html_async_with_buf` in a flat router; any `.await` inside the route's view function during server-side rendering.
Common situations: Server-rendering an app where routes were converted to async routes for browser-side lazy loading; copying a client-only example (which awaits data or dynamic imports) into an SSR setup; using code-splitting/lazy route loading without realizing it is unsupported on the server.
Related errors
- async routes not supported 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/c138feedbfb137bb.
Report an issue: GitHub.