Skip to content

[2.0] Errored recovery waits for a flight that a fresh nested Loading in the new content should catch #4627

Closed

Description

@ryansolid

On next 8180332. An <Errored> shows an error, the errored source is refreshed, and the retry answers with a value. The content that replaces the error contains a <Loading> that has never shown, around a read whose flight starts once the retry answers. The error stays on screen until that inner flight lands. On a first load, the same tree shows the content with the inner fallback straight away.

D-39 and D-41 say a shown error stays until the frame that carries its replacement reveals. The #3540 rule says a Loading mounted while a write is held shows its fallback, and #3977 says a boundary that has not shown content catches a memo's first answer. Read together, I expected the replacement frame to be [Crash][finding related]. If holding the error until the nested content is ready is intended, the rules should say so, because it is the one case where a fresh inner Loading does not catch.

Repro (packages/web/test, jsdom). The output was logged per case, labelled:

import { Loading, Errored, createMemo, flush, refresh } 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 where of ["outside", "inside", "CONTROL"]) test(where, async () => {
  const asks: { key: string; res: (v: any) => void; rej: (e: any) => void }[] = [];
  const ask = (key: string) => new Promise<any>((res, rej) => asks.push({ key, res, rej }));
  const settle = async (key: string, f: (a: any) => void) => { const i = asks.findIndex(a => a.key === key); f(asks.splice(i, 1)[0]); await tick(); };
  const c = document.body.appendChild(document.createElement("div"));
  let detail!: () => any;
  render(() => {
    detail = createMemo(() => ask("detail"));
    const outer = where === "outside" ? createMemo(() => ask(`search:${detail().title}`)) : null!;
    const Related = () => {
      const related = where === "outside" ? outer : createMemo(() => ask(`search:${detail().title}`));
      return <i>[related {related().length}]</i>;
    };
    return <Loading fallback={<i>[loading detail]</i>}>
      <Errored fallback={e => <i>[error {e().message}]</i>}>
        <b>[{detail().title}]</b>
        <Loading fallback={<i>[finding related]</i>}><Related /></Loading>
      </Errored>
    </Loading>;
  }, c);
  await tick();
  if (where !== "CONTROL") {
    await settle("detail", a => a.rej(new Error("not found")));  // [error not found]
    refresh(detail); await tick();                               // [error not found]
  }
  await settle("detail", a => a.res({ title: "Crash" }));
  console.log(where, c.textContent); // expected [Crash][finding related]
  await settle(asks[0].key, a => a.res([1, 2]));
  console.log(where, c.textContent); // [Crash][related 2]
});
case after the retry answers, search in the air after the search answers
search memo created outside Errored [error not found] [Crash][related 2]
search memo created inside the inner Loading [error not found] [Crash][related 2]
CONTROL: first load, no failure [Crash][finding related] [Crash][related 2]

Where it came from: three of the AI-written ReactiveScript apps in our eval round 10 hit this independently. Each issue detail had a related-help search keyed on the loaded title, nested under the detail's error boundary. After "issue not found" → retry → the issue loads, the detail stayed on "not found" until the related search answered, and a never-answering search kept it there. Each agent worked around it by moving the related section out of the error boundary.

Activity

  1. ryansolid commented on Nov 2, 2026

    @ryansolid
    MemberAuthor

    Maintainer ruling (2026-11-02), recorded here because it re-rules async-gen 91501 #1831 (#4401):

    While an <Errored> shows its fallback, nothing under it is on screen, so its recovery re-arms the whole subtree ("back to square one"). Every <Loading> under it counts as not having shown content, even one that showed content before the error. Once the errored source has its new answer, each Loading catches what is still pending in the replacement frame and shows its own fallback beside the recovered content ([Crash][finding related]). After recovery the ordinary rules apply.

    Kept by the ruling ("there are legit reasons to hold the error up in some cases"): the error stays up during the errored source's own re-ask, for a pending read directly under the <Errored> with no <Loading> between (D-39), and for a recovery that is part of a held write or action, which reveals with that hold (D-41).

    The fix is #4634, which also amends D-39.


    Generated by Claude Code

  2. ryansolid commented on Nov 2, 2026

    @ryansolid
    MemberAuthor

    Fixed on next by #4634 (4235019); 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