Repository navigation
Description
Describe the bug
Since 2.0.0-rc.14, a memo created inside a newly opened <Match> crashes the whole app if it reads latest() of an unrelated memo. The update that opens the <Match> also starts a sibling async memo under its own <Loading>. The engine then reads the <Match> callback's accessor after the branch has been closed, which throws "Attempting to access a stale value from that could possibly be undefined". Nothing catches the throw, so the reactive system halts (REACTIVITY_HALTED) and the screen stays on the previous branch for good.
Reading the same memo without latest() works.
Your Example Website or App
Inline below: a self-contained vitest + jsdom test, solid-js / @solidjs/web only.
Steps to Reproduce the Bug or Issue
repro.test.jsx:
import { render } from "@solidjs/web";
import { createMemo, createSignal, latest, Loading, Match, Switch } from "solid-js";
import { expect, it } from "vitest";
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
it("a memo created in a newly opened <Match> may read latest()", async () => {
const el = document.createElement("div");
const [ready, setReady] = createSignal(false);
const zone = createMemo(() => (ready() ? "ready" : "loading"));
const format = createMemo(() => "ru");
function Picker(props) {
// replacing latest(format) with format() makes the test pass
const label = createMemo(() => `${latest(format)}:${zone()}:${props.value()}`);
return <span>{label()}</span>;
}
render(() => {
const data = createMemo(async () => {
if (zone() !== "ready") return "none";
await wait(30);
return "data";
});
return (
<>
<Switch>
<Match when={zone() === "loading"}><i>loading</i></Match>
<Match when={zone() === "ready" && "sel"}>{(s) => <Picker value={s} />}</Match>
</Switch>
<Loading fallback={<b>wait</b>}><b>{data()}</b></Loading>
</>
);
}, el);
setTimeout(() => setReady(true), 10);
await wait(150);
expect(el.textContent).toBe("ru:ready:seldata");
});Setup (package.json + vitest.config.mjs)
{
"private": true,
"type": "module",
"dependencies": {
"solid-js": "2.0.0-rc.19", "@solidjs/web": "2.0.0-rc.19", "@solidjs/signals": "2.0.0-rc.19",
"@solidjs/babel-plugin": "2.0.0-rc.19", "@babel/core": "7.28.5",
"vitest": "3.2.4", "vite": "7.1.12", "jsdom": "26.1.0"
},
"pnpm": { "overrides": { "solid-js": "2.0.0-rc.19", "@solidjs/web": "2.0.0-rc.19", "@solidjs/signals": "2.0.0-rc.19" } }
}// vitest.config.mjs
import { transformAsync } from "@babel/core";
import solid from "@solidjs/babel-plugin";
const conditions = ["browser", "development"];
export default {
plugins: [{
name: "solid-jsx", enforce: "pre",
async transform(code, id) {
if (!id.endsWith(".jsx")) return null;
const r = await transformAsync(code, { filename: id, babelrc: false, configFile: false, sourceMaps: true,
plugins: [[solid, { generate: "dom", hydratable: false }]] });
return { code: r.code, map: r.map };
},
}],
resolve: { conditions },
ssr: { resolve: { conditions, externalConditions: conditions }, noExternal: [/solid-js/, /@solidjs\//] },
test: { environment: "jsdom", server: { deps: { inline: [/solid-js/, /@solidjs\//] } } },
};Run: pnpm install && pnpm vitest run repro.test.jsx. Swap all four Solid versions (and the overrides) to test another RC.
Expected behavior
ru:ready:seldata. The <Match> for ready opens, Picker shows ru:ready:sel, and the <Loading> shows data once it lands. This is what rc.13 renders.
Actual on rc.14 through rc.19 and on next@17d4d1b:
textContentisloadingnone;- the console shows
Attempting to access a stale value from <Match> …, then[REACTIVITY_HALTED] An uncaught error halted the reactive system.
On rc.14 and rc.15, vitest also aborts the worker during teardown (Abort trap: 6).
Screenshots or Videos
No response
Platform
- OS: macOS
- Runtime: Node 25.8.2, vitest 3.2.4, jsdom 26.1.0,
@solidjs/babel-plugin(generatedom). Originally seen in Chromium: a period picker inside a<Switch>halted the page. - Version:
solid-js/@solidjs/web/@solidjs/signals2.0.0-rc.19. Also reproduces onnext@17d4d1b(signals built from source).
Additional context
| solid-js / @solidjs/web / @solidjs/signals | latest(format) |
format() |
|---|---|---|
| 2.0.0-rc.13 | passes | passes |
| 2.0.0-rc.14 … 2.0.0-rc.19 | halts | passes |
next@17d4d1b |
halts | passes |
Bisect. I built packages/signals from source at each signals commit between the rc.13 and rc.14 release commits (35e0651ab7 … c421e199fa), with solid-js and @solidjs/web held at rc.14:
- first bad: 1b9ceb6
signals: the hold model (L2) — core rebuilt from its rulings (#3774); - last good signals commit:
4f676973d3.
rc.19's #4281 (#4275) and #4304 (#4284) change how latest() behaves, but neither fixes this case.
Usage. Each piece is documented:
latest(fn): "Reads the freshest in-flight value available insidefn, falling back to the current settled value."<Match>with a function child receiving the narrowed accessor.createMemoreturning a promise, read under<Loading>.
The component reads the accessor only from its own memo and does not keep it past unmount (no timers or async).
Activity
perf(signals): verdict reads test presence, not null, to pay for #4341
fix(signals): a latest()/isPending() reader born in a newly opened br…
ryansolid commentedon Oct 28, 2026
Fixed on
nextby #4352 (85ad399); it ships in the next RC.Generated by Claude Code
test(signals): reader seat — the slot table and shadow differential a…
fix(signals): a Loading fallback is born held only when the output th…
fix(signals): a verdict reader born into a hold joins it at its re-pa…