Description
Mithril currently doesn't have any error handling story. This is of course really bad.
Here's the concerns I believe need addressed when it comes to fault tolerance, in decreasing priority:
- Mithril shouldn't break over user errors
- Mithril shouldn't hide user errors
- Mithril shouldn't let caught user errors affect unrelated components
- Mithril shouldn't allow potentially invalid state to be used
- Mithril shouldn't let resource leaks propagate
- Mithril shouldn't prevent error recovery
The first two are adequately covered in #1937, but the other 4 are the focus of this bug: helping users write error-tolerant apps.
Edit: Update this for clarity, remove some special semantics.
My proposal is this: add a new error handling sequence algorithm to address all errors, with an associated optional hook onerror: function (vnode, error) { ... }
usable on both components and vnodes.
The error handling sequence is as follows:
- Find the first parent with an
onerror
hook. If no such parent exists, use the root. - Create a reference to the initial error. This is the current error.
- Clear the parent's children, calling
onremove
as appropriate, but notonbeforeremove
. If any hook throws, the hook sets the current error to it and otherwise ignores the error. - If the parent is the root, rethrow the current error.
- Invoke its
onerror
hook with its vnode and the current error. - If the hook throws:
- Set the current error to the thrown error.
- Set the parent to its parent. If no such parent exists, set it to the root.
- Go to step 2.
In code, it would look something like this:
// Where `parentPath` is a linked list of error handlers for this vnode.
function handleError(path, error) {
while (path != null) {
var lastError = {value: error}
onremoveChildren(path.children, lastError)
if (path.vnode == null) throw lastError.value
if (typeof path.vnode.tag !== "string" && typeof path.vnode.state.onerror === "function") {
try {
callHook.call(path.vnode.state.onerror, vnode)
return
} catch (e) {
lastError.value = e
}
}
if (path.vnode.attrs && typeof path.vnode.attrs.onerror === "function") {
try {
callHook.call(path.vnode.attrs.onerror, vnode)
return
} catch (e) {
lastError.value = e
}
}
path = path.parent
}
}
// `onremove` gets fixed to this
function onremoveChildren(children, lastError) {
for (var i = 0; i < children.length; i++) {
if (children[i] != null) onremove(children[i])
}
}
function onremove(vnode, lastError) {
if (vnode.attrs && typeof vnode.attrs.onremove === "function") {
try {
callHook.call(vnode.attrs.onremove, vnode)
} catch (e) {
lastError.value = e
}
}
if (typeof vnode.tag !== "string") {
if (typeof vnode.state.onremove === "function") {
try {
callHook.call(vnode.state.onremove, vnode)
} catch (e) {
lastError.value = e
}
}
if (vnode.instance != null) onremove(vnode.instance)
} else if (Array.isArray(vnode.children)) {
onremoveChildren(vnode.children, lastError)
}
}
Here's when the error handling sequence is executed:
- When creating a child component throws.
- When a child vnode's or component's hook throws, including their
view
oronerror
. - When an
onevent
handler (likeonclick
) on a child DOM vnode throws. - Child vnodes include those returned from the current component's
view
- component boundaries are ignored here.
When it fires, it receives the current vnode (not the origin) and the thrown error. It may choose to do one of two things:
- Rethrow the error to propagate it.
- Return normally to signify recovery.
Here's a couple concrete example of this in action:
// Here's a component that logs errors and reinitializes its tree on error.
var RestartOnError = {
onerror: function (v, e) { console.error(e) },
view: function (v) { return v.attrs.view() },
}
// Here's how you'd use it:
m(RestartOnError, {view: function () {
return m(ComponentThatLikesToError)
}})
// Here's a component that swaps a view on error:
function SwapViewOnError() {
var error = false
return {
onerror: function (v, e) { error = true; console.error(e) },
view: function (v) {
if (!error) {
try {
return v.attrs.view()
} catch (e) {
console.error(e)
error = true
}
}
return v.attrs.errorView()
},
}
}
// Here's how you'd use it:
m(SwapViewOnError, {
view: function () { return m(ComponentThatLikesToError) },
errorView: function () { return "Something bad happened..." },
})
Metadata
Metadata
Assignees
Labels
Type
Projects
Status