Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/writable-memo-write-clears-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/signals": patch
---

A write to a writable memo clears an error its derivation left behind (#4110). Clearing `STATUS_ERROR` only happened on a recompute or an async settle, and a direct write does neither, so a memo that had thrown kept throwing the stale error to every later reader: effects stayed on their error arm, error boundaries never recovered, and the written value sat committed underneath a node that still reported an error. A write equal to the last good value was dropped entirely by the equality gate. Writing to such a memo now recovers it, which is what "a write lands at once and never re-runs `fn` on its own" already promised.
5 changes: 5 additions & 0 deletions packages/signals/src/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
REACTIVE_SNAPSHOT_STALE,
REACTIVE_ZOMBIE,
STATUS_ERROR,
STATUS_NONE,
STATUS_PENDING,
STATUS_UNINITIALIZED,
type Refreshable
Expand Down Expand Up @@ -2154,6 +2155,10 @@ export function setMemo<T>(el: Computed<T>, v: T | ((prev: T) => T)): T {
// The writer read the committed frame; its updater composes on it.
if (held && typeof v === "function") v = (v as (prev: T) => T)(el._value);
const result = setSignal(el, v);
// #4110: the write replaces a failed derivation's outcome. It lands without
// re-running `_fn`, and nothing else clears STATUS_ERROR, so without this
// every later read keeps throwing the error the derivation left behind.
if (el._statusFlags & STATUS_ERROR) notifyStatus(el, STATUS_NONE, undefined);
if (held) {
// The one write that re-runs: the staging it replaced was derived from
// inputs the writer never saw, so the hold re-derives over the write.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* A writable memo that throws keeps throwing for every later reader. Clearing
* STATUS_ERROR only happens on a recompute or an async settle, and a direct
* write deliberately does neither, so the written value commits underneath a
* node that still reports an error and the memo cannot be recovered.
*
* @see https://github.com/solidjs/solid/issues/4110
*/
import { expect, it } from "vitest";
import { createEffect, createRoot, createSignal, flush } from "../src/index.js";

it("a write clears an error the derivation left behind", () => {
const seen: unknown[] = [];
const { value, setValue } = createRoot(() => {
const [failing] = createSignal(true);
const [value, setValue] = createSignal(() => {
if (failing()) throw new Error("boom");
return 1;
});
createEffect(() => value(), {
effect: v => void seen.push({ ok: v }),
error: e => void seen.push({ err: (e as Error).message })
});
return { value, setValue };
});

flush();
expect(seen).toEqual([{ err: "boom" }]);

setValue(2);
flush();
expect(seen).toEqual([{ err: "boom" }, { ok: 2 }]);
expect(value()).toBe(2);
});

it("a write of the last good value still recovers the memo", () => {
const { value, setValue, setFailing } = createRoot(() => {
const [failing, setFailing] = createSignal(false);
const [value, setValue] = createSignal(() => {
if (failing()) throw new Error("boom");
return 1;
});
return { value, setValue, setFailing };
});

expect(value()).toBe(1);

setFailing(true);
flush();
expect(() => value()).toThrow("boom");

// equal to the last good value, so the equality gate must not drop it
setValue(1);
flush();
expect(value()).toBe(1);
});
Loading