;
diff --git a/src/runtime/template_helpers.ts b/src/runtime/template_helpers.ts
index 4164d26a..78db7e03 100644
--- a/src/runtime/template_helpers.ts
+++ b/src/runtime/template_helpers.ts
@@ -4,6 +4,7 @@ import { html } from "./blockdom/index";
import { isOptional, validateSchema } from "./validation";
import type { ComponentConstructor } from "./component";
import { markRaw } from "./reactivity";
+import { OwlError } from "./error_handling";
const ObjectCreate = Object.create;
/**
@@ -70,7 +71,7 @@ function prepareList(collection: any): [any[], any[], number, any[]] {
values = Object.keys(collection);
keys = Object.values(collection);
} else {
- throw new Error("Invalid loop expression");
+ throw new OwlError("Invalid loop expression");
}
const n = values.length;
return [keys, values, n, new Array(n)];
@@ -191,7 +192,7 @@ function multiRefSetter(refs: RefMap, name: string): RefSetter {
if (el) {
count++;
if (count > 1) {
- throw new Error("Cannot have 2 elements with same ref name at the same time");
+ throw new OwlError("Cannot have 2 elements with same ref name at the same time");
}
}
if (count === 0 || el) {
@@ -233,7 +234,7 @@ export function validateProps(name: string | ComponentConstructor
, props:
: name in schema && !("*" in schema) && !isOptional(schema[name]);
for (let p in defaultProps) {
if (isMandatory(p)) {
- throw new Error(
+ throw new OwlError(
`A default value cannot be defined for a mandatory prop (name: '${p}', component: ${ComponentClass.name})`
);
}
@@ -242,7 +243,9 @@ export function validateProps
(name: string | ComponentConstructor
, props:
const errors = validateSchema(props, schema);
if (errors.length) {
- throw new Error(`Invalid props for component '${ComponentClass.name}': ` + errors.join(", "));
+ throw new OwlError(
+ `Invalid props for component '${ComponentClass.name}': ` + errors.join(", ")
+ );
}
}
@@ -264,4 +267,5 @@ export const helpers = {
bind,
createCatcher,
markRaw,
+ OwlError,
};
diff --git a/src/runtime/template_set.ts b/src/runtime/template_set.ts
index 1fc9cf3d..6868e612 100644
--- a/src/runtime/template_set.ts
+++ b/src/runtime/template_set.ts
@@ -3,6 +3,7 @@ import { comment, createBlock, html, list, multi, text, toggler } from "./blockd
import { getCurrent } from "./component_node";
import { Portal, portalTemplate } from "./portal";
import { helpers } from "./template_helpers";
+import { OwlError } from "./error_handling";
const bdom = { text, createBlock, list, multi, html, toggler, comment };
@@ -31,7 +32,7 @@ function parseXML(xml: string): Document {
}
}
}
- throw new Error(msg);
+ throw new OwlError(msg);
}
return doc;
}
@@ -76,7 +77,7 @@ export class TemplateSet {
if (currentAsString === newAsString) {
return;
}
- throw new Error(`Template ${name} already defined with different content`);
+ throw new OwlError(`Template ${name} already defined with different content`);
}
this.rawTemplates[name] = template;
}
@@ -102,7 +103,7 @@ export class TemplateSet {
const componentName = getCurrent().component.constructor.name;
extraInfo = ` (for component "${componentName}")`;
} catch {}
- throw new Error(`Missing template: "${name}"${extraInfo}`);
+ throw new OwlError(`Missing template: "${name}"${extraInfo}`);
}
const isFn = typeof rawTemplate === "function" && !(rawTemplate instanceof Element);
const templateFn = isFn ? rawTemplate : this._compileTemplate(name, rawTemplate);
@@ -119,7 +120,7 @@ export class TemplateSet {
}
_compileTemplate(name: string, template: string | Element): ReturnType {
- throw new Error(`Unable to compile a template. Please use owl full build instead`);
+ throw new OwlError(`Unable to compile a template. Please use owl full build instead`);
}
callTemplate(owner: any, subTemplate: string, ctx: any, parent: any, key: any): any {
diff --git a/src/runtime/utils.ts b/src/runtime/utils.ts
index 990dfc04..9c5c067d 100644
--- a/src/runtime/utils.ts
+++ b/src/runtime/utils.ts
@@ -1,3 +1,4 @@
+import { OwlError } from "./error_handling";
export type Callback = () => void;
/**
@@ -28,10 +29,10 @@ export function batched(callback: Callback): Callback {
export function validateTarget(target: HTMLElement) {
if (!(target instanceof HTMLElement)) {
- throw new Error("Cannot mount component: the target is not a valid DOM element");
+ throw new OwlError("Cannot mount component: the target is not a valid DOM element");
}
if (!document.body.contains(target)) {
- throw new Error("Cannot mount a component on a detached dom node");
+ throw new OwlError("Cannot mount a component on a detached dom node");
}
}
@@ -54,7 +55,7 @@ export function whenReady(fn?: any): Promise {
export async function loadFile(url: string): Promise {
const result = await fetch(url);
if (!result.ok) {
- throw new Error("Error while fetching xml templates");
+ throw new OwlError("Error while fetching xml templates");
}
return await result.text();
}
diff --git a/src/runtime/validation.ts b/src/runtime/validation.ts
index 02d473fb..7d069c56 100644
--- a/src/runtime/validation.ts
+++ b/src/runtime/validation.ts
@@ -1,3 +1,5 @@
+import { OwlError } from "./error_handling";
+
type BaseType =
| typeof String
| typeof Boolean
@@ -70,7 +72,7 @@ function toSchema(spec: SimplifiedSchema): NormalizedSchema {
export function validate(obj: { [key: string]: any }, spec: Schema) {
let errors = validateSchema(obj, spec);
if (errors.length) {
- throw new Error("Invalid object: " + errors.join(", "));
+ throw new OwlError("Invalid object: " + errors.join(", "));
}
}
diff --git a/tests/components/__snapshots__/error_handling.test.ts.snap b/tests/components/__snapshots__/error_handling.test.ts.snap
index f1050afa..2d33186c 100644
--- a/tests/components/__snapshots__/error_handling.test.ts.snap
+++ b/tests/components/__snapshots__/error_handling.test.ts.snap
@@ -101,6 +101,50 @@ exports[`basics simple catchError 2`] = `
}"
`;
+exports[`can catch errors Errors in owl lifecycle are wrapped in dev mode: async hook 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+
+ return function template(ctx, node, key = \\"\\") {
+ return text(ctx['state'].value);
+ }
+}"
+`;
+
+exports[`can catch errors Errors in owl lifecycle are wrapped in dev mode: sync hook 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+
+ return function template(ctx, node, key = \\"\\") {
+ return text(ctx['state'].value);
+ }
+}"
+`;
+
+exports[`can catch errors Errors in owl lifecycle are wrapped out of dev mode: async hook 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+
+ return function template(ctx, node, key = \\"\\") {
+ return text(ctx['state'].value);
+ }
+}"
+`;
+
+exports[`can catch errors Errors in owl lifecycle are wrapped outside dev mode: sync hook 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+
+ return function template(ctx, node, key = \\"\\") {
+ return text(ctx['state'].value);
+ }
+}"
+`;
+
exports[`can catch errors an error in onWillDestroy 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -1257,19 +1301,6 @@ exports[`errors and promises an error in willPatch call will reject the render p
}"
`;
-exports[`errors and promises error type is kept when it is wrapped 1`] = `
-"function anonymous(app, bdom, helpers
-) {
- let { text, createBlock, list, multi, html, toggler, comment } = bdom;
-
- let block1 = createBlock(\`abc
\`);
-
- return function template(ctx, node, key = \\"\\") {
- return block1();
- }
-}"
-`;
-
exports[`errors and promises errors in mounted and in willUnmount 1`] = `
"function anonymous(app, bdom, helpers
) {
diff --git a/tests/components/__snapshots__/t_foreach.test.ts.snap b/tests/components/__snapshots__/t_foreach.test.ts.snap
index 9e6d0fa8..fecb2dfe 100644
--- a/tests/components/__snapshots__/t_foreach.test.ts.snap
+++ b/tests/components/__snapshots__/t_foreach.test.ts.snap
@@ -43,7 +43,7 @@ exports[`list of components crash on duplicate key in dev mode 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
- let { prepareList, withKey } = helpers;
+ let { prepareList, OwlError, withKey } = helpers;
const comp1 = app.createComponent(\`Child\`, true, false, false, true);
return function template(ctx, node, key = \\"\\") {
@@ -53,7 +53,7 @@ exports[`list of components crash on duplicate key in dev mode 1`] = `
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`item\`] = v_block1[i1];
const key1 = 'child';
- if (keys1.has(key1)) { throw new Error(\`Got duplicate key in t-foreach: \${key1}\`)}
+ if (keys1.has(key1)) { throw new OwlError(\`Got duplicate key in t-foreach: \${key1}\`)}
keys1.add(key1);
const props1 = {};
helpers.validateProps(\`Child\`, props1, ctx);
diff --git a/tests/components/error_handling.test.ts b/tests/components/error_handling.test.ts
index 331f10ab..68db350c 100644
--- a/tests/components/error_handling.test.ts
+++ b/tests/components/error_handling.test.ts
@@ -19,6 +19,7 @@ import {
snapshotEverything,
useLogLifecycle,
} from "../helpers";
+import { OwlError } from "../../src/runtime/error_handling";
let fixture: HTMLElement;
@@ -159,16 +160,17 @@ describe("errors and promises", () => {
static template = xml`
`;
}
- let error: Error;
+ let error: OwlError;
try {
await mount(App, fixture);
} catch (e) {
- error = e as Error;
+ error = e as OwlError;
}
expect(error!).toBeDefined();
+ expect(error!.cause).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
- expect(error!.message).toMatch(regexp);
+ expect(error!.cause.message).toMatch(regexp);
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleError).toBeCalledTimes(0);
});
@@ -183,14 +185,15 @@ describe("errors and promises", () => {
}
}
- let error: Error;
+ let error: OwlError;
try {
await mount(App, fixture);
} catch (e) {
- error = e as Error;
+ error = e as OwlError;
}
expect(error!).toBeDefined();
- expect(error!.message).toBe("boom");
+ expect(error!.cause).toBeDefined();
+ expect(error!.cause.message).toBe("boom");
expect(fixture.innerHTML).toBe("");
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(1);
@@ -344,16 +347,17 @@ describe("errors and promises", () => {
static components = { Child };
}
- let error: Error;
+ let error: OwlError;
try {
await mount(App, fixture);
} catch (e) {
- error = e as Error;
+ error = e as OwlError;
}
expect(error!).toBeDefined();
+ expect(error!.cause).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
- expect(error!.message).toMatch(regexp);
+ expect(error!.cause.message).toMatch(regexp);
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(1);
});
@@ -363,7 +367,7 @@ describe("errors and promises", () => {
static template = xml`
`;
flag = false;
setup() {
- onError((e) => (error = e));
+ onError(({ cause }) => (error = cause));
}
}
@@ -390,16 +394,17 @@ describe("errors and promises", () => {
static components = { Child };
}
- let error: Error;
+ let error: OwlError;
try {
await mount(Parent, fixture);
} catch (e) {
- error = e as Error;
+ error = e as OwlError;
}
expect(error!).toBeDefined();
+ expect(error!.cause).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g;
- expect(error!.message).toMatch(regexp);
+ expect(error!.cause.message).toMatch(regexp);
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(1);
});
@@ -506,6 +511,96 @@ describe("can catch errors", () => {
);
});
+ test("Errors in owl lifecycle are wrapped in dev mode: sync hook", async () => {
+ const err = new Error("test error");
+ class Root extends Component {
+ static template = xml``;
+ state = useState({ value: 1 });
+
+ setup() {
+ onMounted(() => {
+ throw err;
+ });
+ }
+ }
+ let e: OwlError;
+ try {
+ await mount(Root, fixture, { test: true });
+ } catch (error) {
+ e = error as OwlError;
+ }
+ expect(e!.message).toBe(`The following error occurred in onMounted: "test error"`);
+ expect(e!.cause).toBe(err);
+ });
+
+ test("Errors in owl lifecycle are wrapped in dev mode: async hook", async () => {
+ const err = new Error("test error");
+ class Root extends Component {
+ static template = xml``;
+ state = useState({ value: 1 });
+
+ setup() {
+ onWillStart(async () => {
+ await nextMicroTick();
+ throw err;
+ });
+ }
+ }
+ let e: OwlError;
+ try {
+ await mount(Root, fixture, { test: true });
+ } catch (error) {
+ e = error as OwlError;
+ }
+ expect(e!.message).toBe(`The following error occurred in onWillStart: "test error"`);
+ expect(e!.cause).toBe(err);
+ });
+
+ test("Errors in owl lifecycle are wrapped outside dev mode: sync hook", async () => {
+ const err = new Error("test error");
+ class Root extends Component {
+ static template = xml``;
+ state = useState({ value: 1 });
+
+ setup() {
+ onMounted(() => {
+ throw err;
+ });
+ }
+ }
+ let e: OwlError;
+ try {
+ await mount(Root, fixture);
+ } catch (error) {
+ e = error as OwlError;
+ }
+ expect(e!.message).toBe("An error occured in the owl lifecycle");
+ expect(e!.cause).toBe(err);
+ });
+
+ test("Errors in owl lifecycle are wrapped out of dev mode: async hook", async () => {
+ const err = new Error("test error");
+ class Root extends Component {
+ static template = xml``;
+ state = useState({ value: 1 });
+
+ setup() {
+ onWillStart(async () => {
+ await nextMicroTick();
+ throw err;
+ });
+ }
+ }
+ let e: OwlError;
+ try {
+ await mount(Root, fixture);
+ } catch (error) {
+ e = error as OwlError;
+ }
+ expect(e!.message).toBe("An error occured in the owl lifecycle");
+ expect(e!.cause).toBe(err);
+ });
+
test("can catch an error in the initial call of a component render function (parent mounted)", async () => {
class ErrorComponent extends Component {
static template = xml`hey
`;
@@ -1204,8 +1299,8 @@ describe("can catch errors", () => {
class Catch extends Component {
static template = xml``;
setup() {
- onError((error) => {
- this.props.onError(error);
+ onError(({ cause }) => {
+ this.props.onError(cause);
});
}
}
diff --git a/tests/components/hooks.test.ts b/tests/components/hooks.test.ts
index 56a0ad58..a5da81b9 100644
--- a/tests/components/hooks.test.ts
+++ b/tests/components/hooks.test.ts
@@ -653,7 +653,7 @@ describe("hooks", () => {
try {
await mount(MyComponent, fixture);
} catch (e: any) {
- expect(e.message).toBe("Intentional error");
+ expect(e.cause.message).toBe("Intentional error");
}
// no console.error because the error has been caught in this test
expect(console.error).toHaveBeenCalledTimes(0);
diff --git a/tests/components/style_class.test.ts b/tests/components/style_class.test.ts
index ddd73a94..4b4d9bc9 100644
--- a/tests/components/style_class.test.ts
+++ b/tests/components/style_class.test.ts
@@ -1,3 +1,4 @@
+import { OwlError } from "../../src/runtime/error_handling";
import { Component, mount, onMounted, useState, xml } from "../../src";
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
@@ -346,16 +347,17 @@ describe("style and class handling", () => {
static template = xml``;
static components = { Child };
}
- let error: Error;
+ let error: OwlError;
try {
await mount(ParentWidget, fixture);
} catch (e) {
- error = e as Error;
+ error = e as OwlError;
}
expect(error!).toBeDefined();
+ expect(error!.cause).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
- expect(error!.message).toMatch(regexp);
+ expect(error!.cause.message).toMatch(regexp);
expect(fixture.innerHTML).toBe("");
expect(mockConsoleWarn).toBeCalledTimes(1);
});
diff --git a/tests/helpers.ts b/tests/helpers.ts
index c41fa801..714653a7 100644
--- a/tests/helpers.ts
+++ b/tests/helpers.ts
@@ -19,6 +19,7 @@ import { helpers } from "../src/runtime/template_helpers";
import { TemplateSet, globalTemplates } from "../src/runtime/template_set";
import { BDom } from "../src/runtime/blockdom";
import { compile } from "../src/compiler";
+import { OwlError } from "../src/runtime/error_handling";
const mount = blockDom.mount;
@@ -221,7 +222,7 @@ export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, v
afterEach(() => {
if (steps.length) {
steps.splice(0);
- throw new Error("Remaining steps! Should be checked by a .toBeLogged() assertion!");
+ throw new OwlError("Remaining steps! Should be checked by a .toBeLogged() assertion!");
}
});
diff --git a/tests/misc/portal.test.ts b/tests/misc/portal.test.ts
index ba6c7c97..ff2dcf3e 100644
--- a/tests/misc/portal.test.ts
+++ b/tests/misc/portal.test.ts
@@ -1,3 +1,4 @@
+import { OwlError } from "../../src/runtime/error_handling";
import {
App,
Component,
@@ -499,7 +500,7 @@ describe("Portal", () => {
`;
state = { error: false };
setup() {
- onError((e) => (error = e));
+ onError(({ cause }) => (error = cause));
}
}
addOutsideDiv(fixture);
@@ -958,14 +959,15 @@ describe("Portal: Props validation", () => {
`;
}
- let error: Error;
+ let error: OwlError;
try {
await mount(Parent, fixture, { dev: true });
} catch (e) {
- error = e as Error;
+ error = e as OwlError;
}
expect(error!).toBeDefined();
- expect(error!.message).toBe(`' ' is not a valid selector`);
+ expect(error!.cause).toBeDefined();
+ expect(error!.cause.message).toBe(`' ' is not a valid selector`);
});
test("target must be a valid selector 2", async () => {