RFC: Improve Error Handling
See original GitHub issueOne of the main goals of Cerebral is to understand the “flow” of your application. Signals give us a nice high level view of our app logic, but often times error handling clutters and adds noise to our otherwise beautiful signals.
A key design goal for error handling should be, in my opinion, that you only need to explicitly handle errors where and when your program requires it. Otherwise, error handling actually detracts from the readability of your program, cluttering it with low level book keeping instead of expressing your intent (which is what Cerebral is all about).
The Problem
Take this (Cerebral 1) signal for example:
module.addSignals({
mounted: [
setupDeviceSettings,
setAuthHeaders,
fetchCurrentUser, {
success: [
copy('input:currentUser', 'state:currentUser'),
copy('state:currentUser.current_account_id', 'state:navbar.newPost.account_id'),
fetchPosts, {
success: [
copy('input:posts', 'state:posts'),
when('flags.showCommentsButton'), {
true: [
fetchComments, {
success: [copy('input:comments', 'state:comments')],
error: [(e) => { console.log('comments error', JSON.stringify(e)); }]
}
],
false: [/*noop*/]
}
],
error: [(e) => { console.log('fetch posts error', JSON.stringify(e)); }]
},
],
error: [(e) => { console.log('fetchCurrentUser error', JSON.stringify(e)); }]
}
]
})
This signal is really just about fetching some related resources from some APIs. We need to get the: currentUser
, their posts
and finally the comments
for their posts. We can’t render the first view until we have that data.
Notice how repetitive the error handling is above and the “pyramid of doom” with indentation creeping to the right of the screen. In this case, the developer is forced to define the error path for each API call and provide an explicit error handling action. But, really, all we care about in this instance is: 1) did everything work? or 2) did something prevent us from getting this data?
The Solution
As we talked about in the hangout, I’d like to start a discussion around a better error handling solution for Cerebral. What if we could re-write the signal above like (please forgive this hodgepodge of Cerebral 1 and 2 syntax 😛) :
module.addSignals({
mounted: sequence([
setupDeviceSettings,
setAuthHeaders,
fetchCurrentUser, {
success: [
copy('input:currentUser', 'state:currentUser'),
copy('state:currentUser.current_account_id', 'state:navbar.newPost.account_id'),
fetchPosts, {
success: [
copy('input:posts', 'state:posts'),
when('flags.showCommentsButton'), {
true: [
fetchComments, {
success: [copy('input:comments', 'state:comments')]
}
],
false: [/*noop*/]
}
]
}
]
}
], {
error: [flash('Error fetching data')]
})
})
The idea is to provide a named path at the top level that can be invoked if any of the nested paths throw an error. Conceptually, a nested error will “bubble up” to the top of the signal chain until it is handled. In the event that you want more specific error handling, you simply define error paths like in the first example. The key thing is that now you are not forced to repeat yourself with error handling when your application doesn’t require you to. Already, with just this one change, I think the logical flow is clearer for “the next developer”. You can more easily scan the signal without mentally removing the error paths.
It’s always a difficult balance between being low level + explicit vs concise/abstract. In this case, however, I think handling errors more concisely would benefit from a higher level API and convention.
Anyway, that’s all I have for now. Everyone please weigh in with ideas, excitement, or objections. 😄
Issue Analytics
- State:
- Created 7 years ago
- Comments:70 (61 by maintainers)
Top GitHub Comments
hah, awesome… it worked on first try… implementation driven development 😄 Implement first, tes after!
Hehe, yeah, abort is implicit… like, by reading the code you never know which one can cause an
abort
to happen. That said, it cleans up so well for these types of scenarios 😃Ah, man, just got very happy. The “merge execution” (client/server) logic is perfect for handling ABORT 😃 Cause it is generic. It basically is a signal that has an “executedBy” prop. Meaning that an abort can fire off a new function tree execution, only it attaches the “executedBy” prop. It will automatically get inlined on the exact action that caused it… do not have to do anything 😃 Only thing is identifying it is an ABORT execution and style it.
Woop woop 🎉