- 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