leptos-rs/leptos · error
children should not be removed until we render here
Error message
children should not be removed until we render here
What it means
In the same SSR Suspense streaming path, after all tasks resolve leptos takes the buffered children out of a Mutex and expects them present: children_lock.take().expect(...). The panic means the children slot was already taken (double render of the same Suspense) or never set before tasks_rx fired — an invariant break in the render lifecycle.
Source
Thrown at leptos/src/suspense_component.rs:463
// that it will wait for those to resolve
select! {
// if there are local resources, bail
// this will only have fired by this point for local resources accessed
// *synchronously*
_ = local_rx => {
let sc = Owner::current_shared_context().expect("no shared context");
sc.set_incomplete_chunk(self.id);
if let Some(tx) =
notify_error_boundary.write_value().take()
{
let _ = tx.send(());
}
None
}
_ = tasks_rx => {
let children = {
let mut children_lock = children.lock().or_poisoned();
children_lock.take().expect("children should not be removed until we render here")
};
// if we ran this earlier, reactive reads would always be registered as None
// this is fine in the case where we want to use Suspend and .await on some future
// but in situations like a <For each=|| some_resource.snapshot()/> we actually
// want to be able to 1) synchronously read a resource's value, but still 2) wait
// for it to load before we render everything
let mut children = Box::pin(children.resolve().fuse());
// we continue racing the children against the "do we have any local
// resources?" Future
select! {
_ = local_rx => {
let sc = Owner::current_shared_context().expect("no shared context");
sc.set_incomplete_chunk(self.id);
if let Some(tx) =
notify_error_boundary.write_value().take()
{View on GitHub (pinned to 32d20f6c9d)
Solutions
- Serialize each Suspense view exactly once per request; build a fresh view per render pass.
- Don't reuse view/component instances across requests — construct them inside the per-request render closure.
- If a retry of a failed stream is needed, recreate the whole view tree rather than re-calling to_html on the same instance.
- Update leptos if this reproduces on a plain single-render path — it indicates a framework lifecycle bug worth reporting.
Example fix
// before
static VIEW: ...; // shared Suspense instance reused across requests
// after
let html = render_to_string(|cx| view! { cx, <Suspense>.../* fresh per request */ });
Defensive patterns
Strategy: validation
Validate before calling
// ensure each Suspense instance is serialized exactly once per request
let view = view! { cx, <Suspense fallback=...>... }; // build fresh per request
let html = render_to_string(move || view.clone()).await; // don't reuse a consumed instance
Prevention
- Construct views per request; never share Suspense instances across renders
- Serialize each response exactly once (no retry on the same instance)
- If a stream fails, rebuild the whole view tree before retrying
- Update leptos to the latest patch — lifecycle double-take bugs are fixed upstream
When it happens
Trigger: Rendering the same Suspense view into HTML twice (calling the serializer twice on one instance); children lock poisoned-adjacent misuse where a previous take consumed the value; custom code re-entering to_html_async_with_buf for the same component.
Common situations: Custom server middleware invoking HTML serialization twice for one request (e.g. fallback + retry path); reusing a view builder across requests; a library bug after partial streaming failure and retry of the same chunk.
Related errors
- no shared context
- Reading from a LocalResource outside Suspense in `ssr` mode
- You cannot use Suspend on an attribute outside Suspense
- You cannot use Suspend on an attribute outside Suspense
- internal error: entered unreachable code
AI-assisted analysis of leptos-rs/leptos@32d20f6c9d (2026-09-01).
Data as JSON: /api/errors/0498cd928e3eb63e.
Report an issue: GitHub.