Repository navigation
Description
Regression. This is broken at next ecf3011 and was fine at e21c2ae. Bisecting puts the change at 749b0d7, #4456 ("a staged error reaches the memos and derived stores that derive from it").
What happens. A <Show> mounts an <Errored> in the same change that its content fails. Inside is a keyed createProjection over a memo, under <Loading>. The memo then re-asks and the new answer lands, but the boundary stays on its fallback. The same thing over the memo directly (no projection) replaces the error as expected.
Where we found it. ReactiveScript's conformance suite, scenario S-ai2-search. A search pane mounts when the query becomes non-empty, a one-character query throws "too short", and typing a second character fetches. The results never replace the hint.
Repro (packages/web/test)
/**
* @jsxImportSource @solidjs/web
* @vitest-environment jsdom
*/
import { test } from "vitest";
import { Show, Errored, Loading, For, createSignal, createMemo, createProjection, flush } from "solid-js";
import { render } from "../src/index.js";
const tick = async () => { for (let i = 0; i < 4; i++) { await new Promise(r => setTimeout(r, 0)); flush(); } };
for (const keyed of [true, false])
test(`an answer replaces the error a boundary was born with (keyed=${keyed})`, async () => {
const asks: { key: string; res: (v: any) => void }[] = [];
const ask = (key: string) => new Promise<any>(res => asks.push({ key, res }));
const land = async (key: string, v: any) => { for (const a of asks.filter(a => a.key === key)) { asks.splice(asks.indexOf(a), 1); a.res(v); } await tick(); };
const container = document.body.appendChild(document.createElement("div"));
const frames: string[] = []; const snap = (l: string) => frames.push(`${l}: ${container.textContent}`);
let setQ!: (q: string) => void;
render(() => {
const [q, sq] = createSignal(""); setQ = sq;
const src = createMemo(() => { const s = q(); if (s === "") return []; if (s.length < 2) throw new Error("too short"); return ask(`search:${s}`); });
const results: () => any[] = keyed ? (() => { const p = createProjection(() => src() as any, [] as any, { key: "id" } as any); return () => p as any; })() : (src as any);
return <Show when={q() !== ""}>
<Errored fallback={(e: any) => <p>{e().message}</p>}>
<Loading fallback={<p>loading</p>}>
<For each={results()}>{(r: any) => <i>{r.name}</i>}</For>
</Loading>
</Errored>
</Show>;
}, container);
await tick(); snap("mount");
setQ("a"); await tick(); snap("query a");
setQ("ap"); await tick(); snap("query ap");
await land("search:ap", [{ id: 1, name: "apple" }, { id: 2, name: "apricot" }]); snap("answer");
console.log(`keyed=${keyed}\n` + frames.join("\n"));
container.remove();
});Output at ecf3011
keyed=true
mount:
query a: too short
query ap: too short
answer: too short <- stays on the error
keyed=false
...
answer: appleapricot
At e21c2ae (the parent of 749b0d7), both cases end with answer: appleapricot.
What it needs
- The boundary has to mount in the same change as the error. Without the
<Show>, so the boundary exists before the query changes, the answer replaces the error. - The read has to go through the keyed projection. Reading the memo directly is fine.
Expected
answer: appleapricot in both cases: a value replaces a shown error (D-41).
Activity
ryansolid commentedon Oct 31, 2026
Fixed on
nextby #4519 (25b94f0); it ships in the next RC.Generated by Claude Code
fix(signals): link a store reader to its derive's staged error (#4517) (