Skip to content

Commit

Permalink
[FIX] runtime: properly handle error caused in mounted hook
Browse files Browse the repository at this point in the history
If a component onMounted hook causes an error, and an error handler
tries to reinitiate a new correct render, the fiber may be recycled. But
the scheduler removes it from its own set of tasks, so it will be
ignored.

We simply check for this condition before removing it from the tasks
set.
  • Loading branch information
ged-odoo committed Nov 22, 2024
1 parent 26c7856 commit b97166d
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 1 deletion.
9 changes: 8 additions & 1 deletion src/runtime/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,14 @@ export class Scheduler {
if (!hasError) {
fiber.complete();
}
this.tasks.delete(fiber);
// at this point, the fiber should have been applied to the DOM, so we can
// remove it from the task list. If it is not the case, it means that there
// was an error and an error handler triggered a new rendering that recycled
// the fiber, so in that case, we actually want to keep the fiber around,
// otherwise it will just be ignored.
if (fiber.appliedToDom) {
this.tasks.delete(fiber);
}
}
}
}
44 changes: 44 additions & 0 deletions tests/components/__snapshots__/error_handling.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,50 @@ exports[`can catch errors can catch an error in a component render function 3`]
}"
`;

exports[`can catch errors can catch an error in onmounted 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(\`Main\`);
if (ctx['state'].ok) {
const Comp1 = ctx['component'];
b3 = toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1));
}
return multi([b2, b3]);
}
}"
`;

exports[`can catch errors can catch an error in onmounted 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;

let block1 = createBlock(\`<div>Error!!!</div>\`);

return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;

exports[`can catch errors can catch an error in onmounted 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;

let block1 = createBlock(\`<div>perfect</div>\`);

return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;

exports[`can catch errors can catch an error in the constructor call of a component render function 1`] = `
"function anonymous(app, bdom, helpers
) {
Expand Down
76 changes: 76 additions & 0 deletions tests/components/error_handling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,82 @@ describe("can catch errors", () => {
expect(mockConsoleWarn).toBeCalledTimes(0);
});

test("can catch an error in onmounted", async () => {
class ErrorComponent extends Component {
static template = xml`<div>Error!!!</div>`;
setup() {
useLogLifecycle();
onMounted(() => {
throw new Error("error");
});
}
}
class PerfectComponent extends Component {
static template = xml`<div>perfect</div>`;
setup() {
useLogLifecycle();
}
}
class Main extends Component {
static template = xml`Main<t t-if="state.ok" t-component="component"/>`;
component: any;
state: any;
setup() {
this.state = useState({ ok: false });
useLogLifecycle();
this.component = ErrorComponent;
onError(() => {
this.component = PerfectComponent;
this.render();
});
}
}

const app = await mount(Main, fixture);
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Main:setup",
"Main:willStart",
"Main:willRender",
"Main:rendered",
"Main:mounted",
]
`);
expect(fixture.innerHTML).toBe("Main");
(app as any).state.ok = true;
await nextTick();
expect(fixture.innerHTML).toBe("Main<div>Error!!!</div>");
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Main:willRender",
"ErrorComponent:setup",
"ErrorComponent:willStart",
"Main:rendered",
"ErrorComponent:willRender",
"ErrorComponent:rendered",
"Main:willPatch",
"ErrorComponent:mounted",
"Main:willRender",
"PerfectComponent:setup",
"PerfectComponent:willStart",
"Main:rendered",
]
`);
await nextTick();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"PerfectComponent:willRender",
"PerfectComponent:rendered",
"Main:willPatch",
"ErrorComponent:willUnmount",
"ErrorComponent:willDestroy",
"PerfectComponent:mounted",
"Main:patched",
]
`);
expect(fixture.innerHTML).toBe("Main<div>perfect</div>");
});

test("calling a hook outside setup should crash", async () => {
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
Expand Down

0 comments on commit b97166d

Please sign in to comment.