Skip to content

[2.0] Sibling Loading on boundaries hold silently when one reads its async through createErrorBoundary #4638

Closed

Description

@ryansolid

On next 3ca5262. Two sibling <Loading on={sel()}> boundaries. One reads an async memo through createErrorBoundary (used as a value-level catch), and the other reads a plain async memo keyed by the same signal. When sel changes, neither boundary shows its fallback. The old content stays on screen until both answers land, so the transition holds silently.

If you remove the error boundary, both boundaries show their fallbacks at once. A single boundary that reads through the error boundary also works (it shows loading #1). The failing combination is the error boundary read plus a sibling Loading on that is pending.

/** @jsxImportSource @solidjs/web  @vitest-environment jsdom */
import { test } from "vitest";
import { Loading, createErrorBoundary, createMemo, createSignal, flush, untrack } 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 caught of [true, false]) test(`switch key (${caught ? "error boundary" : "plain"})`, async () => {
  const asks: { key: string; res: (v: any) => void }[] = [];
  const ask = (key: string) => new Promise<any>(res => asks.push({ key, res }));
  const settle = async (key: string, v: any) => { asks.splice(asks.findIndex(a => a.key === key), 1)[0].res(v); await tick(); };
  const c = document.body.appendChild(document.createElement("div"));
  let select!: (n: number) => void;
  render(() => {
    const [sel, setSel] = createSignal(3); select = setSel;
    const detail = createMemo(() => ask(`detail:${sel()}`));
    const shown = caught ? untrack(() => createErrorBoundary(() => detail(), () => null)) : detail;
    const comments = createMemo(() => ask(`comments:${sel()}`));
    return <>
      <Loading on={sel()} fallback={<i>loading #{sel()}</i>}><b>{(shown() as any)?.title}</b></Loading>
      <Loading on={sel()} fallback={<i> loading comments</i>}><u> comments {(comments() as any[]).length}</u></Loading>
    </>;
  }, c);
  await tick();
  await settle("detail:3", { title: "Crash" }); await settle("comments:3", [1]);
  select(1); await tick();
  console.log(caught ? "[error boundary]" : "[plain]", c.textContent);
});

Output after select(1), while detail:1 and comments:1 are in flight:

[error boundary] Crash comments 1                 <- expected: loading #1 loading comments
[plain]          loading #1 loading comments

Found in ReactiveScript's AI eval (round 10, desk rx-1). There, okDetail = (asked catch (e) => null) lowers to createErrorBoundary. The issue switch never showed "loading #N", and the error lines for the old issue stayed up until the new issue's answers landed. If the agent's app uses an errored region instead of the catch expression, it shows the fallback at once.

Activity

  1. ryansolid commented on Nov 3, 2026

    @ryansolid
    MemberAuthor

    Fixed on next by #4639 (6e569c7); 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