Repository navigation
solid-query 6.0.0-rc.5: onCacheEvent throws NotReadyError during a Solid 2 (rc.14+) transition landing #12031
Description
Describe the bug
With Solid 2 rc.14+ (solid-js / @solidjs/signals 2.0.0-rc.15), useBaseQuery's onCacheEvent can throw NotReadyError while a transition lands. The throw escapes unhandled, aborts the landing, and the next transition never commits.
The chain:
- A transition lands, and an unmounting
useQueryruns itsonCleanup:observer.destroy()→QueryCache.notify. - The notify reaches a sibling
useQueryon the same key that was created inside that (still held) transition. - Its
onCacheEventcomparesevent.query.queryHash === untrack(defaultedOptions).queryHash.defaultedOptionsis a memo born held, and since rc.14 an untracked read of such a node throwsNotReadyError(Solid 2 diagnostic A19, exception 1).
In an app with @solidjs/router this looks like a navigation that never finishes.
Your minimal, reproducible example
Inline below (vitest + jsdom; no app code). jsdom is needed because isServer is typeof window === "undefined", and without it the cache subscription is never created.
Steps to reproduce
vitest.config.mjs:
import { defineConfig } from "vitest/config";
const dev = process.env.MODE === "dev";
const conditions = dev ? ["development", "browser"] : ["browser"];
export default defineConfig({
test: {
pool: "forks",
environment: "jsdom",
server: { deps: { inline: [/solid-js/, /@solidjs/, /@tanstack\/solid-query/] } },
},
resolve: { conditions },
ssr: { resolve: { conditions, externalConditions: conditions } },
});repro.test.mjs:
import { QueryClient, useQuery } from "@tanstack/solid-query";
import { createMemo, createRenderEffect, createRoot, createSignal, flush, onCleanup } from "solid-js";
import { expect, it } from "vitest";
const tick = () => new Promise((r) => setTimeout(r, 0));
it("query torn down on landing must not abort it for a sibling born in the transition", async () => {
const gates = [];
const releaseAll = async () => {
for (let i = 0; i < 6; i++) {
for (const open of gates.splice(0)) open();
await tick();
flush();
await tick();
}
};
const flight = (k) => (k > 0 ? new Promise((r) => gates.push(r)).then(() => k) : Promise.resolve(k));
const client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Infinity } } });
const [s, setS] = createSignal(0);
const shown = [];
const dispose = createRoot((done) => {
const screen = createMemo(() => {
const k = s();
const q = useQuery(() => ({ queryKey: ["repro"], queryFn: async () => "answer" }), () => client);
return () => (q.isSuccess ? k : -1);
});
const pending = createMemo(async () => flight(s()));
createRenderEffect(
() => [screen()(), pending()],
([k]) => { shown.push(k); },
);
return done;
});
flush();
await releaseAll();
setS(1); flush(); await tick();
setS(2); flush(); await releaseAll();
setS(3); flush(); await releaseAll();
expect(s()).toBe(3);
expect(shown.at(-1)).toBe(3);
dispose();
client.clear();
});Run with MODE=prod NODE_ENV=production npx vitest run repro.test.mjs, or MODE=dev NODE_ENV=development for the dev build.
| patches applied | prod build | dev build |
|---|---|---|
| none | assertion fails (s() stays 2), unhandled NotReadyError |
same |
| adapter change below only | passes, no error | same |
| Solid fix from solidjs/solid#4089 only | assertion passes, NotReadyError still unhandled |
same |
| both | passes, no error | same |
We confirmed the source of the throw by temporarily wrapping untrack(defaultedOptions) in onCacheEvent with a try/catch, which caught NotReadyError on cache events during the landing.
Expected behavior
The test passes: onCacheEvent does not throw during a landing, and the third transition commits (s() === 3, last rendered value 3).
Possible fix
observer.options is what the committed render effect last passed to observer.setOptions, so it holds the hook's current hash in a plain object instead of the reactive graph:
- if (event.query.queryHash === untrack(defaultedOptions).queryHash || event.query.queryHash === latestHash) {
+ if (event.query.queryHash === observer.options.queryHash || event.query.queryHash === latestHash) {With this change the reproduction passes in both builds. We have not run the solid-query test suite against it.
How often does this bug happen?
Every time
Platform
- Node v25.8.2, macOS; vitest 4.1.11, jsdom 29.1.1. The navigation hang also shows up in Chrome.
Tanstack Query adapter
solid-query
TanStack Query version
@tanstack/solid-query 6.0.0-rc.5 (@tanstack/query-core 5.101.4)
Additional context
solid-js2.0.0-rc.15 /@solidjs/signals2.0.0-rc.15. Not reproducible on rc.13, which has no throwing branch for untracked reads of held nodes.- Related Solid issue on the same landing path: signals rc.14+: a write from onCleanup during land() resurrects the landed transaction, and later transitions never commit solidjs/solid#4089. Fixing either side alone does not cover the other: the Solid fix hides the stuck transition, but the
NotReadyErrorfrom the adapter still escapes. - Possibly related: solid-query: EFFECT_RELAY_TEAR on Solid 2 when a query key changes — useBaseQuery relays the result through a version signal written from a render effect #11903 (a different symptom of the same
useBaseQueryrelay under Solid 2).
Activity
UzumakiODZ commentedon Oct 15, 2026
I would like to work on this issue
niukanen1 commentedon Oct 15, 2026
I looked into this and opened #12034 - happy to adjust if you had something else in mind.