diff --git a/src/runtime/fibers.ts b/src/runtime/fibers.ts index 13b2124da..7dea4a466 100644 --- a/src/runtime/fibers.ts +++ b/src/runtime/fibers.ts @@ -30,6 +30,13 @@ export function makeRootFiber(node: ComponentNode): Fiber { fibersInError.delete(current); fibersInError.delete(root); current.appliedToDom = false; + if (current instanceof RootFiber) { + // it is possible that this fiber is a fiber that crashed while being + // mounted, so the mounted list is possibly corrupted. We restore it to + // its normal initial state (which is empty list or a list with a mount + // fiber. + current.mounted = current instanceof MountFiber ? [current] : []; + } } return current; } @@ -152,6 +159,7 @@ export class RootFiber extends Fiber { const node = this.node; this.locked = true; let current: Fiber | undefined = undefined; + let mountedFibers = this.mounted; try { // Step 1: calling all willPatch lifecycle hooks for (current of this.willPatch) { @@ -173,7 +181,6 @@ export class RootFiber extends Fiber { this.locked = false; // Step 4: calling all mounted lifecycle hooks - let mountedFibers = this.mounted; while ((current = mountedFibers.pop())) { current = current; if (current.appliedToDom) { @@ -194,6 +201,15 @@ export class RootFiber extends Fiber { } } } catch (e) { + // if mountedFibers is not empty, this means that a crash occured while + // calling the mounted hooks of some component. So, there may still be + // some component that have been mounted, but for which the mounted hooks + // have not been called. Here, we remove the willUnmount hooks for these + // specific component to prevent a worse situation (willUnmount being + // called even though mounted has not been called) + for (let fiber of mountedFibers) { + fiber.node.willUnmount = []; + } this.locked = false; node.app.handleError({ fiber: current || this, error: e }); } diff --git a/tests/components/__snapshots__/error_handling.test.ts.snap b/tests/components/__snapshots__/error_handling.test.ts.snap index 45f1bf43d..20ff00465 100644 --- a/tests/components/__snapshots__/error_handling.test.ts.snap +++ b/tests/components/__snapshots__/error_handling.test.ts.snap @@ -1191,6 +1191,135 @@ exports[`can catch errors error in mounted on a component with a sibling (proper }" `; +exports[`can catch errors error in onMounted, graceful recovery 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + const comp1 = app.createComponent(null, false, false, false, []); + + return function template(ctx, node, key = \\"\\") { + const Comp1 = ctx['component']; + return toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1)); + } +}" +`; + +exports[`can catch errors error in onMounted, graceful recovery 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + const comp1 = app.createComponent(\`Child\`, true, false, false, []); + const comp2 = app.createComponent(\`Boom\`, true, false, false, []); + + return function template(ctx, node, key = \\"\\") { + const b2 = text(\`parent\`); + const b3 = comp1({}, key + \`__1\`, node, this, null); + const b4 = comp2({}, key + \`__2\`, node, this, null); + return multi([b2, b3, b4]); + } +}" +`; + +exports[`can catch errors error in onMounted, graceful recovery 3`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + return function template(ctx, node, key = \\"\\") { + return text(\`abc\`); + } +}" +`; + +exports[`can catch errors error in onMounted, graceful recovery 4`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + return function template(ctx, node, key = \\"\\") { + return text(\`boom\`); + } +}" +`; + +exports[`can catch errors error in onMounted, graceful recovery 5`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + return function template(ctx, node, key = \\"\\") { + return text(\`def\`); + } +}" +`; + +exports[`can catch errors error in onMounted, graceful recovery, variation 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + const comp1 = app.createComponent(null, false, false, false, []); + + return function template(ctx, node, key = \\"\\") { + let b2, b3; + b2 = text(\`R\`); + if (ctx['state'].gogogo) { + const Comp1 = ctx['component']; + b3 = toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1)); + } + return multi([b2, b3]); + } +}" +`; + +exports[`can catch errors error in onMounted, graceful recovery, variation 3`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + const comp1 = app.createComponent(\`Child\`, true, false, false, []); + const comp2 = app.createComponent(\`Boom\`, true, false, false, []); + + return function template(ctx, node, key = \\"\\") { + const b2 = text(\`parent\`); + const b3 = comp1({}, key + \`__1\`, node, this, null); + const b4 = comp2({}, key + \`__2\`, node, this, null); + return multi([b2, b3, b4]); + } +}" +`; + +exports[`can catch errors error in onMounted, graceful recovery, variation 4`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + return function template(ctx, node, key = \\"\\") { + return text(\`abc\`); + } +}" +`; + +exports[`can catch errors error in onMounted, graceful recovery, variation 5`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + return function template(ctx, node, key = \\"\\") { + return text(\`boom\`); + } +}" +`; + +exports[`can catch errors error in onMounted, graceful recovery, variation 6`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + return function template(ctx, node, key = \\"\\") { + return text(\`def\`); + } +}" +`; + exports[`can catch errors onError in class inheritance is called if rethrown 1`] = ` "function anonymous(app, bdom, helpers ) { diff --git a/tests/components/error_handling.test.ts b/tests/components/error_handling.test.ts index 703b29183..672544277 100644 --- a/tests/components/error_handling.test.ts +++ b/tests/components/error_handling.test.ts @@ -1678,4 +1678,198 @@ describe("can catch errors", () => { `); expect(fixture.innerHTML).toBe("2"); }); + + test("error in onMounted, graceful recovery", async () => { + class Child extends Component { + static template = xml`abc`; + setup() { + useLogLifecycle(); + } + } + + class OtherChild extends Component { + static template = xml`def`; + setup() { + useLogLifecycle(); + } + } + + class Boom extends Component { + static template = xml`boom`; + setup() { + useLogLifecycle(); + onMounted(() => { + throw new Error("boom"); + }); + } + } + + class Parent extends Component { + static template = xml`parent`; + static components = { Child, Boom }; + setup() { + useLogLifecycle(); + } + } + + class Root extends Component { + static template = xml``; + + component: any = Parent; + setup() { + useLogLifecycle(); + onError(() => { + logStep("error"); + this.component = OtherChild; + this.render(); + }); + } + } + + await mount(Root, fixture); + expect(fixture.innerHTML).toBe("def"); + + expect(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "Root:setup", + "Root:willStart", + "Root:willRender", + "Parent:setup", + "Parent:willStart", + "Root:rendered", + "Parent:willRender", + "Child:setup", + "Child:willStart", + "Boom:setup", + "Boom:willStart", + "Parent:rendered", + "Child:willRender", + "Child:rendered", + "Boom:willRender", + "Boom:rendered", + "Boom:mounted", + "error", + "Root:willRender", + "OtherChild:setup", + "OtherChild:willStart", + "Root:rendered", + "OtherChild:willRender", + "OtherChild:rendered", + "OtherChild:mounted", + "Root:mounted", + ] + `); + }); + + test("error in onMounted, graceful recovery, variation", async () => { + class Child extends Component { + static template = xml`abc`; + setup() { + useLogLifecycle(); + } + } + + class OtherChild extends Component { + static template = xml`def`; + setup() { + useLogLifecycle(); + } + } + + class Boom extends Component { + static template = xml`boom`; + setup() { + useLogLifecycle(); + onMounted(() => { + throw new Error("boom"); + }); + } + } + + class Parent extends Component { + static template = xml`parent`; + static components = { Child, Boom }; + setup() { + useLogLifecycle(); + } + } + + class Root extends Component { + static template = xml`R`; + + component: any = Parent; + state = useState({ gogogo: false }); + + setup() { + useLogLifecycle(); + onError(() => { + logStep("error"); + this.component = OtherChild; + this.render(); + }); + } + } + + const root = await mount(Root, fixture); + expect(fixture.innerHTML).toBe("R"); + + // standard mounting process + expect(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "Root:setup", + "Root:willStart", + "Root:willRender", + "Root:rendered", + "Root:mounted", + ] + `); + + root.state.gogogo = true; + await nextTick(); + + expect(fixture.innerHTML).toBe("Rparentabcboom"); + // rerender, root creates sub components, it crashes, tries to recover + expect(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "Root:willRender", + "Parent:setup", + "Parent:willStart", + "Root:rendered", + "Parent:willRender", + "Child:setup", + "Child:willStart", + "Boom:setup", + "Boom:willStart", + "Parent:rendered", + "Child:willRender", + "Child:rendered", + "Boom:willRender", + "Boom:rendered", + "Root:willPatch", + "Boom:mounted", + "error", + "Root:willRender", + "OtherChild:setup", + "OtherChild:willStart", + "Root:rendered", + ] + `); + + await nextTick(); + expect(fixture.innerHTML).toBe("Rdef"); + + expect(steps.splice(0)).toMatchInlineSnapshot(` + Array [ + "OtherChild:willRender", + "OtherChild:rendered", + "Root:willPatch", + "Child:willDestroy", + "Boom:willUnmount", + "Boom:willDestroy", + "Parent:willDestroy", + "OtherChild:mounted", + "Root:patched", + ] + `); + }); });