Repository navigation
Description
Rule. A boundary that takes a failure during the server render is streamed with its fallback drawn, so the browser shows the client's frame, and the client adopts it rather than asking again (ReactiveScript 10 SV-6, SV-3b).
Shape. <Errored fallback={e => <p>failed: {e().message}</p>}> around a
<Loading> whose content reads an async memo; the memo rejects with an
error marked safe (markSafeError), either before the shell flushes
(ms = 1) or after (ms = 200, the default).
Observed.
client component runInServerComponentScope
fails after shell <template id="100"> </template> <template id="100"><p>failed: no user 7</p></template>
fails before shell <main></main> <main><p>failed: no user 7</p></main>
Outside server-component scope the boundary's HTML is empty and the error
is serialized for the client; until hydration the browser shows neither the loading fallback nor the error
fallback, a frame no client tick shows. errorOutcome in ssrLoadingBoundary and renderOutcome in the
server createErrorBoundary both return early unless
inServerComponentScope.
After hydration (dv9-hydrate/). The client renders the failed
boundary from scratch: it asks the question again and shows the loading
fallback, never the failure the server sent.
shell "loading"
the read fails (streamed) ""
hydrated "loading"
Expected: the error fallback is drawn on the server in both scopes, and the client adopts it. (A failure thrown synchronously in content the server renders is drawn.)
Reproduced on next @ 2a0da2f. Run from a checkout with solid-js, @solidjs/web and @solidjs/compiler built from next: node dv9-ssr-after-shell.mjs [client|server-component] [ms] (server build); node dv9-hydrate/run.mjs (needs playwright + esbuild).
Repro
dv9-ssr-after-shell.mjs
// ms: when the read rejects — 200 (default) is after the shell has flushed, 1 is before.
import { createMemo, Loading, Errored, createComponent, runInServerComponentScope } from "solid-js";
import { renderToStream, ssr, escape, markSafeError } from "@solidjs/web";
const mode = process.argv[2] ?? "client"; const ms = Number(process.argv[3] ?? 200);
let reject; const p = new Promise((_, j) => (reject = j)); p.catch(() => {});
function App() {
const user = createMemo(() => p);
return ssr(["<main>", "</main>"], createComponent(Errored, {
fallback: e => ssr(["<p>failed: ", "</p>"], escape(e().message)),
get children() { return createComponent(Loading, { fallback: ssr(["<p>loading</p>"]), get children() { return ssr(["<p>", "</p>"], () => escape(user().name)); } }); },
}));
}
const view = () => createComponent(App, {});
renderToStream(mode === "server-component" ? () => runInServerComponentScope(view) : view)
.pipe({ write: c => console.log(c.replace(/<script>[\s\S]*?<\/script>/g, "")), end() {} });
setTimeout(() => reject(markSafeError(new Error("no user 7"))), ms); // marked safe: a failure the client may seedv9-hydrate/app.jsx
import { createMemo } from "solid-js";
export function App(props) {
const user = createMemo(() => props.ask("u"));
return <Errored fallback={e => <p>failed: {e().message}</p>}>
<Loading fallback={<p>loading</p>}><p>{user()}</p></Loading>
</Errored>;
}dv9-hydrate/run.mjs
// The server's read fails after the shell (a safe error, so the client may see it); the stream sends an empty fragment
// and the error. Then the client hydrates. Expected: "failed: no user", the server's answer used, not asked again.
import { markSafeError } from "@solidjs/web";
import { harness, wait } from "../_hydrate.mjs";
let reject;
const h = await harness(new URL(".", import.meta.url).pathname, () => new Promise((_, j) => { reject = j; }));
const page = await h.open("hydrate");
const show = async label => console.log(label.padEnd(26), JSON.stringify(await page.evaluate(() => window.repro.text())));
await show("shell");
reject(markSafeError(new Error("no user"))); await wait(100);
await show("the read fails (streamed)");
await page.evaluate(() => window.repro.start("hydrate")); await wait(100);
await show("hydrated");
console.log("asked on the client:".padEnd(26), JSON.stringify(await page.evaluate(() => window.repro.asked())), "(one is the trace Solid runs under a mocked fetch; expected: the server's error, no new question)");
await h.close();_hydrate.mjs is the hydration harness posted in full in #4272.
Found by ReactiveScript's conformance suite, which diffs its spec against Solid's SCENARIOS.md.
Activity
fix(solid): an Errored around a failing streamed Loading draws its fa…
ryansolid commentedon Oct 26, 2026
Fixed on
nextby #4303 (f5cf675); it ships in the next RC.Generated by Claude Code
fix(solid,web): a failed Errored waits for its fallback's async holes…
fix(solid): hydration never asks again for an answer the server alrea…
fix(solid): a rejected fragment's fresh render claims what the server…