Skip to content
Merged
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/error-boundary-fallback-rethrow-recovers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/signals": patch
---

An error boundary that caught an error rethrown from an inner boundary's fallback now recovers when the inner content does: switching the inner boundary back to its content disposes the fallback that threw, and the outer boundary now drops that error and shows its content again in the same flush, instead of keeping its fallback until some later error re-rendered it (#4270).
4 changes: 4 additions & 0 deletions packages/signals/src/boundaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,10 @@ function createBoundary<T>(
if (fb !== null)
globalQueue.enqueue(EFFECT_RENDER, () => {
if (!b._fallback && b._fb === fb) {
// A fallback that threw (a rethrow the boundary does not
// handle) is what an enclosing boundary waits on: it died, and
// the seam of another round drops it there (#4270).
if (fb._statusFlags & STATUS_ERROR) schedule();
dispose(fb);
b._fb = null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// #4270: an error rethrown from an inner boundary's fallback is caught by the
// outer boundary, whose source is the inner fallback's computed. The inner
// content recovering disposes that computed; the outer must recover with it.
import {
createErrorBoundary,
createMemo,
createRenderEffect,
createRoot,
createSignal,
flush,
untrack
} from "../src/index.js";

class NotFound extends Error {}
class Other extends Error {}

it.each([false, true])(
"an outer boundary recovers when the inner fallback that rethrew is replaced by content (nested: %s)",
nested => {
const [which, setWhich] = createSignal(0);
let screen = "";
createRoot(() => {
const m = createMemo(() => {
const w = which();
if (w === 1) throw new NotFound("nf");
if (w === 2) throw new Other("other");
return "ok" + w;
});
const makeInner = () =>
createErrorBoundary(
() => m(),
err => {
const e = err();
if (!(e instanceof NotFound)) throw e;
return "inner:" + e.message;
}
);
const inner = nested ? null : untrack(makeInner);
const outer = untrack(() =>
createErrorBoundary(
nested ? makeInner : () => inner!(),
err => "outer:" + (err() as Error).message
)
);
createRenderEffect(
() => outer(),
v => {
screen = v;
}
);
});
const seen: string[] = [];
flush();
seen.push(screen);
for (const w of [1, 0, 2, 0, 1, 0]) {
setWhich(w);
flush();
seen.push(screen);
}
expect(seen).toEqual(["ok0", "inner:nf", "ok0", "outer:other", "ok0", "inner:nf", "ok0"]);
}
);
Loading