Our error-handling is broken (Re: Promise slides)

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