From fb7d25ba0e7bbea3536443a47ca60bbe78d4dbfb Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Tue, 30 Sep 2025 15:46:18 +0200 Subject: [PATCH] [FIX] error_handling: onError works if handling happens between culprit and root Have a component implementing onError that calls a callback from one of its parent. That parent should not be the original source of the rendering (a parent above is). Make the callback handle the error and trigger an render() Before this commit, the parent did not see it was handling a subtree in error, and did not have a chance to revert that error state in its rendering stack. After this commit, this flow works. --- src/runtime/error_handling.ts | 1 + .../__snapshots__/error_handling.test.ts.snap | 46 +++++++++++++++++++ tests/components/error_handling.test.ts | 38 +++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/src/runtime/error_handling.ts b/src/runtime/error_handling.ts index 275e138b..e264eb84 100644 --- a/src/runtime/error_handling.ts +++ b/src/runtime/error_handling.ts @@ -55,6 +55,7 @@ export function handleError(params: ErrorParams) { let current: Fiber | null = fiber; do { current.node.fiber = current; + fibersInError.set(current, error); current = current.parent; } while (current); diff --git a/tests/components/__snapshots__/error_handling.test.ts.snap b/tests/components/__snapshots__/error_handling.test.ts.snap index 20ff0046..acacbeb0 100644 --- a/tests/components/__snapshots__/error_handling.test.ts.snap +++ b/tests/components/__snapshots__/error_handling.test.ts.snap @@ -94,6 +94,52 @@ exports[`basics no component catching error lead to full app destruction 2`] = ` }" `; +exports[`basics render from above on error -- handler is not a Root or MountFiber 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + const comp1 = app.createComponent(\`Parent\`, true, false, false, []); + + return function template(ctx, node, key = \\"\\") { + return comp1({}, key + \`__1\`, node, this, null); + } +}" +`; + +exports[`basics render from above on error -- handler is not a Root or MountFiber 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + const comp1 = app.createComponent(\`Boom\`, true, false, false, []); + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let b2, b3; + if (ctx['error']) { + b2 = text(\`Error\`); + } else { + b3 = comp1({onError: (ctx['handleError']).bind(this)}, key + \`__1\`, node, this, null); + } + return block1([], [b2, b3]); + } +}" +`; + +exports[`basics render from above on error -- handler is not a Root or MountFiber 3`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let txt1 = ctx['a'].b.c; + return block1([txt1]); + } +}" +`; + exports[`basics simple catchError 1`] = ` "function anonymous(app, bdom, helpers ) { diff --git a/tests/components/error_handling.test.ts b/tests/components/error_handling.test.ts index 67254427..7b512b2b 100644 --- a/tests/components/error_handling.test.ts +++ b/tests/components/error_handling.test.ts @@ -233,6 +233,44 @@ function(app, bdom, helpers) { expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0); }); + + test("render from above on error -- handler is not a Root or MountFiber", async () => { + class Boom extends Component { + static template = xml`
`; + setup() { + onError((err) => { + this.props.onError(err); + }); + } + } + + class Parent extends Component { + static template = xml` +
+ Error + + + +
`; + static components = { Boom }; + + error: any = false; + + handleError(err: Error) { + this.error = err; + this.render(); + } + } + + class GrandParent extends Component { + static template: string = xml``; + static components = { Parent }; + } + await mount(GrandParent, fixture); + expect(fixture.innerHTML).toBe("
Error
"); + expect(mockConsoleError).toBeCalledTimes(0); + expect(mockConsoleWarn).toBeCalledTimes(0); + }); }); describe("errors and promises", () => {