diff --git a/.changeset/projection-staged-error-reader-link-4517.md b/.changeset/projection-staged-error-reader-link-4517.md
new file mode 100644
index 0000000000..5170e6c34b
--- /dev/null
+++ b/.changeset/projection-staged-error-reader-link-4517.md
@@ -0,0 +1,5 @@
+---
+"@solidjs/signals": patch
+---
+
+A reader of a derived store whose derive carries a staged error (D-41) now links to the derive, as a reader of a pending or errored one does, so it re-runs when the derive's source answers. An `` mounted in the change that errored its content over a keyed `createProjection` showed its fallback for good; it now shows the answer.
diff --git a/packages/signals/src/store/store.ts b/packages/signals/src/store/store.ts
index d12476c0fd..9d318b674a 100644
--- a/packages/signals/src/store/store.ts
+++ b/packages/signals/src/store/store.ts
@@ -36,6 +36,7 @@ import {
CONFIG_MANUAL_WRITE,
CONFIG_OVERRIDE,
CONFIG_PROMOTED,
+ CONFIG_STAGED_ERROR,
CONFIG_VERDICT,
EFFECT_RENDER,
NOT_PENDING,
@@ -1844,7 +1845,8 @@ function pullFamily(target: StoreTarget): void {
// reader of a memo would — linked, so it re-runs at the landing (the
// next settled pass pulls without linking and the stale link trims), a
// verdict reader registered, a tracked pass suspended.
- if (fw._statusFlags & (STATUS_PENDING | STATUS_ERROR)) {
+ // A staged error (D-41) throws at the pull, before any leaf would link the reader.
+ if (fw._statusFlags & (STATUS_PENDING | STATUS_ERROR) || fw._config & CONFIG_STAGED_ERROR) {
// A render effect outside the flight's own flush is the frame, not a
// derivation: it keeps what it shows and learns of the landing from the
// leaves the landing changes (unchanged leaves say nothing — it is not a
@@ -1858,7 +1860,7 @@ function pullFamily(target: StoreTarget): void {
if (
c !== null &&
c._type === EFFECT_RENDER &&
- !(fw._statusFlags & STATUS_ERROR) &&
+ fw._statusFlags & STATUS_PENDING &&
fw._config & CONFIG_HELD &&
flushTransaction !== txOf(fw) &&
!linkedTo(c, fw)
diff --git a/packages/signals/tests/errored-born-staged-error-projection-4517.test.ts b/packages/signals/tests/errored-born-staged-error-projection-4517.test.ts
new file mode 100644
index 0000000000..1fb438ad01
--- /dev/null
+++ b/packages/signals/tests/errored-born-staged-error-projection-4517.test.ts
@@ -0,0 +1,86 @@
+import { expect, it } from "vitest";
+import {
+ createErrorBoundary,
+ createLoadingBoundary,
+ createMemo,
+ createProjection,
+ createRenderEffect,
+ createRoot,
+ createSignal,
+ flush,
+ mapArray,
+ untrack
+} from "../src/index.js";
+
+const tick = async () => {
+ for (let i = 0; i < 4; i++) {
+ await new Promise(r => setTimeout(r, 0));
+ flush();
+ }
+};
+
+type Row = { id: number; name: string };
+
+for (const keyed of [true, false])
+ it(`an answer replaces the error a boundary was born with (projection: ${keyed}) (#4517)`, async () => {
+ const asks: { key: string; res: (v: Row[]) => void }[] = [];
+ const ask = (key: string) => new Promise(res => asks.push({ key, res }));
+ const land = async (key: string, v: Row[]) => {
+ for (const a of asks.filter(a => a.key === key)) {
+ asks.splice(asks.indexOf(a), 1);
+ a.res(v);
+ }
+ await tick();
+ };
+ const [q, setQ] = createSignal("");
+ let screen: unknown;
+ const dispose = createRoot(d => {
+ const src = createMemo(() => {
+ const s = q();
+ if (s === "") return [] as Row[];
+ if (s.length < 2) throw new Error("too short");
+ return ask(`search:${s}`);
+ });
+ const results: () => Row[] = keyed
+ ? (() => {
+ const p = createProjection(() => src(), [] as Row[], { key: "id" });
+ return () => p;
+ })()
+ : src;
+ const show = createMemo(() => q() !== "");
+ const out = createMemo(() => {
+ if (!show()) return undefined;
+ return untrack(() =>
+ createErrorBoundary(
+ () =>
+ createLoadingBoundary(
+ () => mapArray(results, r => r.name),
+ () => "loading"
+ ),
+ e => `E:${(e() as Error).message}`
+ )
+ );
+ });
+ const resolve = (v: unknown): unknown =>
+ typeof v === "function" ? resolve(v()) : Array.isArray(v) ? v.join("") : v;
+ createRenderEffect(
+ () => resolve(out()),
+ v => void (screen = v)
+ );
+ return d;
+ });
+ await tick();
+ expect(screen).toBe(undefined);
+ setQ("a");
+ await tick();
+ expect(screen).toBe("E:too short");
+ setQ("ap");
+ await tick();
+ expect(screen).toBe("E:too short");
+ await land("search:ap", [
+ { id: 1, name: "apple" },
+ { id: 2, name: "apricot" }
+ ]);
+ expect(screen).toBe("appleapricot");
+ dispose();
+ });
diff --git a/packages/web/test/errored-born-staged-error-projection-4517.spec.tsx b/packages/web/test/errored-born-staged-error-projection-4517.spec.tsx
new file mode 100644
index 0000000000..ccd46a1784
--- /dev/null
+++ b/packages/web/test/errored-born-staged-error-projection-4517.spec.tsx
@@ -0,0 +1,79 @@
+/**
+ * @jsxImportSource @solidjs/web
+ * @vitest-environment jsdom
+ */
+import { expect, test } from "vitest";
+import { render } from "../src/index.js";
+import {
+ createMemo,
+ createProjection,
+ createSignal,
+ Errored,
+ flush,
+ For,
+ Loading,
+ Show
+} from "solid-js";
+
+type Row = { id: number; name: string };
+
+const tick = async () => {
+ for (let i = 0; i < 4; i++) {
+ await new Promise(r => setTimeout(r, 0));
+ flush();
+ }
+};
+
+test.each([true, false])(
+ "an answer replaces the error a boundary was born with (projection: %s)",
+ async projection => {
+ const asks: { key: string; res: (v: Row[]) => void }[] = [];
+ const ask = (key: string) => new Promise(res => asks.push({ key, res }));
+ const land = async (key: string, v: Row[]) => {
+ for (const a of asks.filter(a => a.key === key)) {
+ asks.splice(asks.indexOf(a), 1);
+ a.res(v);
+ }
+ await tick();
+ };
+ const container = document.createElement("div");
+ const [q, setQ] = createSignal("");
+ const dispose = render(() => {
+ const src = createMemo(() => {
+ const s = q();
+ if (s === "") return [] as Row[];
+ if (s.length < 2) throw new Error("too short");
+ return ask(`search:${s}`);
+ });
+ const results: () => Row[] = projection
+ ? (() => {
+ const p = createProjection(() => src(), [] as Row[], { key: "id" });
+ return () => p;
+ })()
+ : src;
+ return (
+
+ any) => {e().message}
}>
+ loading
}>
+ {r => {r.name}}
+
+
+
+ );
+ }, container);
+ await tick();
+ expect(container.textContent).toBe("");
+ setQ("a");
+ await tick();
+ expect(container.textContent).toBe("too short");
+ setQ("ap");
+ await tick();
+ expect(container.textContent).toBe("too short");
+ await land("search:ap", [
+ { id: 1, name: "apple" },
+ { id: 2, name: "apricot" }
+ ]);
+ expect(container.textContent).toBe("appleapricot");
+ dispose();
+ }
+);