From 922eb7cd980d617f5ada9dc4a10ccdc9c82599f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 18 Jun 2021 10:21:32 +0200 Subject: [PATCH] [FIX] component: propagate errors to parent Before this commit, the error handling process was too naive: once an error occurs in a rendering, owl catches it, looks for a component that implements the catchError method, then calls it. However, in real life, we sometimes need to rethrow that error (or another one) to propagate the error to some parent handler. This error needs to be handled by the closest parent component that implements catchError. This is what this commit implements: it wraps the catchError call in a try/catch, then in case of errors, try to handle it by a parent. --- src/component/fiber.ts | 32 ++++++++---- tests/component/error_handling.test.ts | 68 +++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 10 deletions(-) diff --git a/src/component/fiber.ts b/src/component/fiber.ts index c6b47bdc..ee76c01f 100644 --- a/src/component/fiber.ts +++ b/src/component/fiber.ts @@ -326,16 +326,30 @@ export class Fiber { const qweb = component.env.qweb; let root = component; - let canCatch = false; - while (component && !(canCatch = !!component.catchError)) { - root = component; - component = component.__owl__.parent!; - } - qweb.trigger("error", error); - if (canCatch) { - component.catchError!(error); - } else { + function handle(error) { + let canCatch = false; + qweb.trigger("error", error); + while (component && !(canCatch = !!component.catchError)) { + root = component; + component = component.__owl__.parent!; + } + if (canCatch) { + try { + component.catchError!(error); + } catch (e) { + root = component; + component = component.__owl__.parent!; + return handle(e); + } + return true; + } + return false; + } + + let isHandled = handle(error); + + if (!isHandled) { // the 3 next lines aim to mark the root fiber as being in error, and // to force it to end, without waiting for its children this.root.counter = 0; diff --git a/tests/component/error_handling.test.ts b/tests/component/error_handling.test.ts index 191c939f..666da5a3 100644 --- a/tests/component/error_handling.test.ts +++ b/tests/component/error_handling.test.ts @@ -1,4 +1,4 @@ -import { Component, Env, STATUS } from "../../src/component/component"; +import { Component, Env, mount, STATUS } from "../../src/component/component"; import { useState } from "../../src/hooks"; import { xml } from "../../src/tags"; import { makeTestEnv, makeTestFixture, nextTick } from "../helpers"; @@ -520,4 +520,70 @@ describe("component error handling (catchError)", () => { expect(error).toBeDefined(); expect(error.message).toBe("Cannot read property 'y' of undefined"); }); + + test("simple catchError", async () => { + class Boom extends Component { + static template = xml`
`; + } + + class Parent extends Component { + static template = xml` +
+ Error + + + +
`; + static components = { Boom }; + + error = false; + + catchError(error) { + this.error = error; + this.render(); + } + } + + await mount(Parent, { target: fixture }); + expect(fixture.innerHTML).toBe("
Error
"); + }); + + test("catchError in catchError", async () => { + class Boom extends Component { + static template = xml`
`; + } + + class Child extends Component { + static template = xml` +
+ +
`; + static components = { Boom }; + + catchError(error) { + throw error; + } + } + + class Parent extends Component { + static template = xml` +
+ Error + + + +
`; + static components = { Child }; + + error = false; + + catchError(error) { + this.error = error; + this.render(); + } + } + + await mount(Parent, { target: fixture }); + expect(fixture.innerHTML).toBe("
Error
"); + }); });