Skip to content

Commit

Permalink
[IMP] component: add catchError hook
Browse files Browse the repository at this point in the history
  • Loading branch information
ged-odoo committed Jul 12, 2019
1 parent f4cd111 commit d9cbd23
Show file tree
Hide file tree
Showing 3 changed files with 468 additions and 49 deletions.
85 changes: 76 additions & 9 deletions doc/component.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- [References](#references)
- [Slots](#slots)
- [Asynchronous Rendering](#asynchronous-rendering)
- [Error Handling](#error-handling)

## Overview

Expand Down Expand Up @@ -188,15 +189,16 @@ A solid and robust component system needs useful hooks/methods to help
developers write components. Here is a complete description of the lifecycle of
a owl component:
| Method | Description |
| ------------------------------------------------ | ----------------------------------------------------- |
| **[constructor](#constructorparent-props)** | constructor |
| **[willStart](#willstart)** | async, before first rendering |
| **[mounted](#mounted)** | just after component is rendered and added to the DOM |
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
| **[willPatch](#willpatch)** | just before the DOM is patched |
| **[patched](#patchedsnapshot)** | just after the DOM is patched |
| **[willUnmount](#willunmount)** | just before removing component from DOM |
| Method | Description |
| ------------------------------------------------ | ------------------------------------------------------------ |
| **[constructor](#constructorparent-props)** | constructor |
| **[willStart](#willstart)** | async, before first rendering |
| **[mounted](#mounted)** | just after component is rendered and added to the DOM |
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
| **[willPatch](#willpatch)** | just before the DOM is patched |
| **[patched](#patchedsnapshot)** | just after the DOM is patched |
| **[willUnmount](#willunmount)** | just before removing component from DOM |
| **[catchError](#catcherrorerror)** | catch errors (see [error handling section](#error-handling)) |
Notes:
Expand Down Expand Up @@ -337,6 +339,12 @@ the DOM. This is a good place to remove some listeners, for example.
This is the opposite method of `mounted`.
#### `catchError(error)`
The `catchError` method is useful when we need to intercept and properly react
to (rendering) errors that occur in some sub components. See the section on
[error handling](#error-handling)
### Root Component
Most of the time, an Owl component will be created automatically by a tag (or the `t-component`
Expand Down Expand Up @@ -1020,3 +1028,62 @@ Here are a few tips on how to work with asynchronous components:
<AsyncChild t-asyncroot="1"/>
</div>
```
### Error Handling
By default, whenever an error occurs in the rendering of an Owl application, we
destroy the whole application. Otherwise, we cannot offer any guarantee on the
state of the resulting component tree. It might be hopelessly corrupted, but
without any user-visible state.
Clearly, it sometimes is a little bit extreme to destroy the application. This
is why we have a builtin mechanism to handle rendering errors (and errors coming
from lifecycle hooks): the `catchError` hook.
Whenever the `catchError` lifecycle hook is implemented, all errors coming from
sub components rendering and/or lifecycle method calls will be caught and given
to the `catchError` method. This allow us to properly handle the error, and to
not break the application.
For example, here is how we could implement an `ErrorBoundary` component:
```xml
<div t-name="ErrorBoundary">
<t t-if="state.error">
Error handled
</t>
<t t-else="1">
<t t-slot="default" />
</t>
</div>
```
```js
class ErrorBoundary extends Widget {
state = { error: false };

catchError() {
this.state.error = true;
}
}
```
Using the `ErrorBoundary` is then extremely simple:
```xml
<ErrorBoundary><SomeOtherComponent/></ErrorBoundary>
```
Note that we need to be careful here: the fallback UI should not throw any
error, otherwise we risk going into an infinite loop.
Also, it may be useful to know that whenever an error is caught, it is then
broadcasted to the application by an event on the `qweb` instance. It may be
useful, for example, to log the error somewhere.
```js
env.qweb.on("error", null, function(error) {
// do something
// react to the error
});
```
108 changes: 83 additions & 25 deletions src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,12 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
*/
willUnmount() {}

/**
* catchError is a method called whenever some error happens in the rendering or
* lifecycle hooks of a child.
*/
catchError(error: Error): void {}

//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
Expand Down Expand Up @@ -398,7 +404,11 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
for (let key in handlers) {
handlers[key]();
}
this.mounted();
try {
this.mounted();
} catch (e) {
errorHandler(e, this);
}
}

__callWillUnmount() {
Expand All @@ -419,7 +429,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
forceUpdate: boolean = false,
patchQueue?: any[],
scope?: any,
vars?: any,
vars?: any
): Promise<void> {
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
if (shouldUpdate) {
Expand Down Expand Up @@ -451,7 +461,12 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}

async __prepareAndRender(scope?: Object, vars?: any): Promise<VNode> {
await this.willStart();
try {
await this.willStart();
} catch (e) {
errorHandler(e, this);
return Promise.resolve(h("div"));
}
const __owl__ = this.__owl__;
if (__owl__.isDestroyed) {
return Promise.resolve(h("div"));
Expand Down Expand Up @@ -485,7 +500,12 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
return this.__render(false, [], scope, vars);
}

async __render(force: boolean = false, patchQueue: any[] = [], scope?: Object, vars?: any): Promise<VNode> {
async __render(
force: boolean = false,
patchQueue: any[] = [],
scope?: Object,
vars?: any
): Promise<VNode> {
const __owl__ = this.__owl__;
__owl__.renderId++;
const promises: Promise<void>[] = [];
Expand All @@ -496,15 +516,21 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (__owl__.observer) {
__owl__.observer.allowMutations = false;
}
let vnode = __owl__.render!(this, {
promises,
handlers: __owl__.boundHandlers,
mountedHandlers: __owl__.mountedHandlers,
forceUpdate: force,
patchQueue,
scope,
vars
});
let vnode;
try {
vnode = __owl__.render!(this, {
promises,
handlers: __owl__.boundHandlers,
mountedHandlers: __owl__.mountedHandlers,
forceUpdate: force,
patchQueue,
scope,
vars
});
} catch (e) {
vnode = __owl__.vnode || h("div");
errorHandler(e, this);
}
patch.push(vnode);
if (__owl__.observer) {
__owl__.observer.allowMutations = true;
Expand Down Expand Up @@ -580,18 +606,50 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* 3) Call 'patched' on the component of each patch, in inverse order
*/
__applyPatchQueue(patchQueue: any[]) {
const patchLen = patchQueue.length;
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
patch.push(patch[0].willPatch());
}
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
patch[0].__patch(patch[1]);
}
for (let i = patchLen - 1; i >= 0; i--) {
const patch = patchQueue[i];
patch[0].patched(patch[2]);
let component = this;
try {
const patchLen = patchQueue.length;
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
component = patch[0];
patch.push(patch[0].willPatch());
}
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
patch[0].__patch(patch[1]);
}
for (let i = patchLen - 1; i >= 0; i--) {
const patch = patchQueue[i];
component = patch[0];
patch[0].patched(patch[2]);
}
} catch (e) {
errorHandler(e, component);
}
}
}

//------------------------------------------------------------------------------
// Error handling
//------------------------------------------------------------------------------

function errorHandler(error, component) {
let canCatch = false;
let qweb = component.env.qweb;
let root = component;
while (component && !(canCatch = component.catchError !== Component.prototype.catchError)) {
root = component;
component = component.__owl__.parent;
}
console.error(error);
// we trigger error on QWeb so it can be logged/handled
qweb.trigger("error", error);

if (canCatch) {
setTimeout(() => {
component.catchError(error);
});
} else {
root.destroy();
}
}
Loading

0 comments on commit d9cbd23

Please sign in to comment.