Skip to content

[2.0 / next] Regression (#4456): an Errored mounted with an error stays on its fallback after a keyed projection's answer lands #4517

Closed

Description

@ryansolid

Regression. This is broken at next ecf3011 and was fine at e21c2ae. Bisecting puts the change at 749b0d7, #4456 ("a staged error reaches the memos and derived stores that derive from it").

What happens. A <Show> mounts an <Errored> in the same change that its content fails. Inside is a keyed createProjection over a memo, under <Loading>. The memo then re-asks and the new answer lands, but the boundary stays on its fallback. The same thing over the memo directly (no projection) replaces the error as expected.

Where we found it. ReactiveScript's conformance suite, scenario S-ai2-search. A search pane mounts when the query becomes non-empty, a one-character query throws "too short", and typing a second character fetches. The results never replace the hint.

Repro (packages/web/test)

/**
 * @jsxImportSource @solidjs/web
 * @vitest-environment jsdom
 */
import { test } from "vitest";
import { Show, Errored, Loading, For, createSignal, createMemo, createProjection, flush } from "solid-js";
import { render } from "../src/index.js";
const tick = async () => { for (let i = 0; i < 4; i++) { await new Promise(r => setTimeout(r, 0)); flush(); } };
for (const keyed of [true, false])
test(`an answer replaces the error a boundary was born with (keyed=${keyed})`, async () => {
  const asks: { key: string; res: (v: any) => void }[] = [];
  const ask = (key: string) => new Promise<any>(res => asks.push({ key, res }));
  const land = async (key: string, v: any) => { for (const a of asks.filter(a => a.key === key)) { asks.splice(asks.indexOf(a), 1); a.res(v); } await tick(); };
  const container = document.body.appendChild(document.createElement("div"));
  const frames: string[] = []; const snap = (l: string) => frames.push(`${l}: ${container.textContent}`);
  let setQ!: (q: string) => void;
  render(() => {
    const [q, sq] = createSignal(""); setQ = sq;
    const src = createMemo(() => { const s = q(); if (s === "") return []; if (s.length < 2) throw new Error("too short"); return ask(`search:${s}`); });
    const results: () => any[] = keyed ? (() => { const p = createProjection(() => src() as any, [] as any, { key: "id" } as any); return () => p as any; })() : (src as any);
    return <Show when={q() !== ""}>
      <Errored fallback={(e: any) => <p>{e().message}</p>}>
        <Loading fallback={<p>loading</p>}>
          <For each={results()}>{(r: any) => <i>{r.name}</i>}</For>
        </Loading>
      </Errored>
    </Show>;
  }, container);
  await tick(); snap("mount");
  setQ("a"); await tick(); snap("query a");
  setQ("ap"); await tick(); snap("query ap");
  await land("search:ap", [{ id: 1, name: "apple" }, { id: 2, name: "apricot" }]); snap("answer");
  console.log(`keyed=${keyed}\n` + frames.join("\n"));
  container.remove();
});

Output at ecf3011

keyed=true
mount:
query a: too short
query ap: too short
answer: too short          <- stays on the error
keyed=false
...
answer: appleapricot

At e21c2ae (the parent of 749b0d7), both cases end with answer: appleapricot.

What it needs

  • The boundary has to mount in the same change as the error. Without the <Show>, so the boundary exists before the query changes, the answer replaces the error.
  • The read has to go through the keyed projection. Reading the memo directly is fine.

Expected

answer: appleapricot in both cases: a value replaces a shown error (D-41).

Activity

  1. ryansolid commented on Oct 31, 2026

    @ryansolid
    MemberAuthor

    Fixed on next by #4519 (25b94f0); it ships in the next RC.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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