- From: Jan-Ivar Bruaroey <jib@mozilla.com>
- Date: Wed, 01 Oct 2014 16:05:35 -0400
- To: "public-media-capture@w3.org" <public-media-capture@w3.org>
- Message-ID: <542C5E8F.7050507@mozilla.com>
In case it's not obvious from the slides, I'll be arguing tomorrow that
our error-handling is broken and should not be shipped.
This excellent post on promises [1] says why better than I can:
> "There are two very important aspects of synchronous functions:
>
> They return values
> They throw exceptions
>
> Both of these are essentially about composition. That is, you can feed
> the return value of one function straight into another, and keep doing
> this indefinitely. More importantly, if at any point that process
> fails, one function in the composition chain can throw an exception,
> which then bypasses all further compositional layers until it comes
> into the hands of someone who can handle it with a catch."
>
> Now, in an asynchronous world, you can no longer return values: they
> simply aren't ready in time. Similarly, you can't throw exceptions,
> because nobody's there to catch them. So we descend into the so-called
> "callback hell," where composition of return values involves nested
> callbacks, and composition of errors involves passing them up the
> chain manually, and oh by the way you'd better never throw an
> exception or else you'll need to introduce something crazy like domains.
I will demonstrate.
First, here's a fiddle [2] showing how promises handle errors correctly:
<div id="log"></div>
var div = document.getElementById("log");
var log = msg => (div.innerHTML = div.innerHTML + msg + "<br>");
new Promise(resolve => resolve())
.then(() => log("success1"), () => log("fail1"))
.then(() => {
log("success2a");
barf;
log("success2b");
}, () => log("fail2"))
.then(() => log("success3"), () => log("fail3"))
.then(() => log("success4"), () => log("fail4"))
.then(() => log("success5"), () => log("fail5"))
.catch(() => log("failure"));
This outputs:
success1
success2a
fail3
success4
success5
Importantly, fail3 - not fail2 - is called because success2 which barfed
is actually step3.
Furthermore, success4 and success5 proceed because we "caught" the error
in fail3! (we'd need to re-throw the error just like we'd do in a
try-catch clause if we didn't want that).
Now, here's the same test in a fiddle [3] showing how callbacks handle
errors poorly (spot the bug):
<div id="log"></div>
var div = document.getElementById("log");
var log = msg => (div.innerHTML = div.innerHTML + msg + "<br>");
try {
oldcall(() => {
log("success1");
oldcall(() => {
log("success2a");
barf;
log("success2b");
oldcall(() => {
log("success3");
oldcall(() => {
log("success4");
oldcall(() => {
log("success5");
}, () => log("fail5"));
}, () => log("fail4"));
}, () => log("fail3"));
}, () => log("fail2"));
}, () => log("fail1"));
} catch(e) { log("failure"); }
function oldcall(success, failure) {
var succeed = true;
setTimeout(succeed? success : failure, 0);
}
This outputs:
success1
success2a
and a "ReferenceError: barf is not defined" in web console.
This is bad because it means the program can't handle the error. The
bug? try/catch is needed around success2 and - to be safe - around
*every* callback! This final fiddle [4] shows what's minimally needed to
handle errors safely in our API, and even this doesn't truly propagate
errors, e.g. we're just pushing handlers in, not allowing errors to rise up:
var div = document.getElementById("log");
var log = msg => (div.innerHTML = div.innerHTML + msg + "<br>");
try {
oldcall(() => { try {
log("success1");
oldcall(() => { try {
log("success2a");
barf;
log("success2b");
oldcall(() => { try {
log("success3");
oldcall(() => { try {
log("success4");
oldcall(() => { try {
log("success5");
} catch(e) { log("failure"); } }, () => log("fail5"));
} catch(e) { log("fail5"); } }, () => log("fail4"));
} catch(e) { log("fail4"); } }, () => log("fail3"));
} catch(e) { log("fail3"); } }, () => log("fail2"));
} catch(e) { log("fail2"); } }, () => log("fail1"));
} catch(e) { log("failure"); }
function oldcall(success, failure) {
var succeed = true;
setTimeout(succeed? success : failure, 0);
}
This error-prone boilerplate is reminiscent of antique languages without
exception-handling, and has this output:
success1
success2a
fail3
This STILL doesn't have the same output as the first fiddle, and I gave
up since I don't think a version that does is feasible without promises.
Could we at least hide the boilerplate inside oldcall? We could, but
this would require a third argument (recall that we want fail3 not fail2):
function oldcall(success, failure, followingFailure) {
try {
var succeed = true;
setTimeout(succeed? success : failure, 0);
} catch (e) { followingFailure(e); }
}
The third argument would have to be optional to be backwards compatible,
so there would be no enforcement. Even then it would be complicated to
use, and still wouldn't solve propagation.
The proper way to solve this is with promises.
.: Jan-Ivar :.
[1]
http://domenic.me/2012/10/14/youre-missing-the-point-of-promises/#what-is-the-point-of-promises
[2] Firefox: http://jsfiddle.net/jib1/eyq80vh6 - Others:
http://jsfiddle.net/jib1/68h59cbf
[3] Firefox: http://jsfiddle.net/jib1/79qLeb4g - Others:
http://jsfiddle.net/jib1/qyqypsyv
[4] Firefox: http://jsfiddle.net/jib1/cy4rvpLb - Others:
http://jsfiddle.net/jib1/xh5r933a
Received on Wednesday, 1 October 2014 20:06:05 UTC