I have two components that are rendered into two different dom nodes with two separate render calls.
If an Error occurs in the render method of the first component (and is caught), the second component will still render, but it's componentDidMount lifecycle method will never be called. If the broken component is rendered to the dom after the good component, there is no problem.
I created a minimal example here to reproduce the issue.
Just clone the repo and run yarn start to start up the dev server.
You will see that the componentDidMount method of the good component is not called.
If you change the order of the rendering in src/index.js it renders correctly.
Edit - added a jsfiddle as well - https://jsfiddle.net/6dhpkxav/3/
Interesting artifact for sure! Definitely a bug, it's because we use a shared queue that is only flushed at the end of render() and it's bailing prior leaving the queue dirty.
That said, I would strongly recommend handling errors differently in order to avoid the issue entirely. Doing so allows you to track the error source and even continue to render in the event of a component failure. I've used this in the past with some statefulness to actually have broken renders fall back to the most recent working render.
import { options } from 'preact';
let old = options.vnode;
options.vnode = vnode => {
let tag = vnode.nodeName;
if (typeof tag==='function') {
if (tag.__wrapped!=null) tag = tag.__wrapped;
else {
tag = tag.__wrapped = tag.prototype && tag.prototype.render ? wrapClass(tag) : wrapFn(tag);
}
vnode.nodeName = tag;
}
if (old) old(vnode);
};
function wrapFn(fn) {
return function(props, context) {
try {
return fn(props, context);
} catch(e) { console.error(`<${fn.displayName || fn.name}> error: ${e}`); }
};
}
function wrapClass(ctor) {
function Safe(props, context) {
try {
return new ctor(props, context)
} catch(e) { console.error(`${ctor.displayName || ctor.name} error: ${e}`); }
}
copy(Safe, ctor, safeWrap);
copy(Safe.prototype, ctor.prototype, safeWrap);
Safe.prototype.constructor = Safe;
return Safe;
}
function safeWrap(fn, name) {
return function() {
try {
return fn.apply(this, arguments);
} catch(e) {
console.error(`${this.constructor.displayName || this.constructor.name}.${name}() error: ${e}`);
}
};
}
function copy(dest, source, map) {
for (let i in source) {
if (source.hasOwnProperty(i)) {
dest[i] = map(source[i], i);
}
}
}
Thanks for the update and the suggestion. In our particular case we are rendering multiple widgets on to a page and each one is wrapped by a common component, so I can work around this issue by handling it there.
We also don't have too many state changes so I think I would prefer the component fail fast and hard, it might lead to some tricky debugging if we added logic to fall back to the last good render. For our case at least.
Thanks for all your work on preact, it's a great project.
Any updates on this bug?
I ran into the same issue but I don't see a clean way how to avoid this issue in my application. I still want to catch and handle such errors outside the preact components, so I want preact.render to throw in case one of my components has an error.
Here where I work, we need to render multiple components written by many teams and even teams which not work at the same company. These components are maybe in React, Preact, Angular, vanilla or something else and I just provide these libraries to them. However, we noticed that a broken preact component was affecting the next ones. I can't presume that everyone will handle it at the top-level because it doesn't scale.
Because of that, I've gone further in debugging and got a precise diagnostic to this case:
Even when calling a new top-level render, the variable diffLevel is shared between different rendering trees because it's declared on the module.
export let diffLevel = 0;
Every time a component's constructor/render method is called and a error is throw, this line breaks up, leading us to a case where diffLevel will not be decremented as it should be.
if (!--diffLevel) {
By that, diffLevel will never be zero again and the queue of componentDidMount wont be flushed.
if (!componentRoot) flushMounts();
As I see, there are two ways to solve it: isolate the diffLevel by tree render or handle possible uncaught errors. I prefer the first one because the second may introduce performance penalties.
@dfleury see https://github.com/developit/preact/pull/1018
I'm encountering this issue too - Is there any official comment on whether PhilippMi's pull request is going to be merged, or if the issue will be addressed in any other way?
The proper way to handle errors that occur inside render is via componentDidCatch 馃帀 With it you can even render custom fallback content which is very neat for UX 鉁旓笍
Most helpful comment
@dfleury see https://github.com/developit/preact/pull/1018