Skip to content

[2.0 / next] A memo reading latest(a) beside b() computes a torn pass on a batched write; if that pass throws, the error sticks #4365

Closed

Description

@ryansolid

Found by the async app generator (packages/web/test/async-gen, branch claude/project-thread-bgafe5), minimized. There is no async anywhere in this case.

const [a, setA] = createSignal(1);
const [b, setB] = createSignal(0);
render(() => {
  const m = createMemo(() => {
    const v = latest(a) + b() + 1;
    if (v % 4 === 3) throw new Error("m:" + v);
    return v;
  });
  return <Errored fallback={e => `[E ${e()?.message}]`}><span title={String(m())} /></Errored>;
}, el);
flush();
setB(3); setA(2); flush();
// m computes latest(a)=2 with b()=0 → 3 → throws, and never re-runs: screen stays [E m:3]

Expected: m = 2 + 3 + 1 = 6, <span title="6">. A fresh render with a=2 and b=3 shows that.

Actual on next (1f558e5): the memo runs one pass with the new latest(a) beside the old b(). When that pass throws, the boundary keeps the error, and nothing re-runs m with b=3.

Without the throw, the torn pass still reaches effects. In a memo returning [latest(s), s()] after set(2), a render effect commits [2,1] before [2,2]. That is a torn tuple from a plain write with no transition held, which "latest tears" (#4284) doesn't seem to cover.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions