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

  1. 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.
  2. Move lazy loading client-side only (e.g. gate dynamic loading behind `is_server()`/cfg so the server uses the already-loaded component).
  3. 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

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


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