Synchronous invocation in JavaScript, part 4: error handling

Last time we saw how to transfer results of computations from pseudo-blocking calls back into the main process. What happens if when something goes wrong, though?

In the (real) synchronous world, error conditions used to travel on the same road as return values:


    char *foo;
    foo = doSomething();
    if (foo == NULL) {
        error("can't get foo");
        exit(1);
    } else {
        doSomethingElseWith(foo);
    }

Then someone realized that error handling and the real task would better not be intertwined, or the general sense of the latter might become less and less clear. After all, errors were, er, the exception, not the rule, and might best be handled in a parallel world that veery now and then resurfaced, but not in the middle of nice-looking, expressive code.


    try {
        var foo = doSomething();
        doSomethingElseWith(foo);
    } catch(e if e == NoFooException) {
        error("can't get foo");
    }

Normally, asynchronous code in JavaScript puts us back in the first scenario, although we don’t get the result/error through a return value but as a function argument:


    var req = new XMLHttpRequest();
    req.open('GET', url, true);
    req.onreadystatechange = function(event) {
        if(req.readyState == 4) {
            if(req.status != 200)
                error('Something went wrong!');
            else
                doSomethingWith(req);
        }
    };
    req.send(null);

Turning it into pseudo-synchronous makes things only a bit better. The error comes through a return value and we deal with errors exactly as in the first scenario:


    function getUrl(url) {
        return function(driver) {
            var req = new XMLHttpRequest();
            req.open('GET', url, true);
            req.onreadystatechange = function(event) {
                if(req.readyState == 4)
                    driver(req);
            };
            req.send(null);
        }
    }

    var driver = proc(function() {
        dump("Retrieving URL...\n");
        var req = yield(getUrl(''));
        if(req.status != 200)
            dump('error!');
        else
            dump(req.responseText + '\n');
    });

    driver();

What we want, though, is to make use of the exception “channel”, for the reasons given above: separating description of the task from description of what to do when something goes wrong.

It takes some extra load in the process definition that hopefully isn’t too hard on the eyes:


    var driver = proc(function() {
        try {
            var result = check(yield(
                errorProneFunction()
            ));
            dump('Success! ' + result);
        } catch(e) {
            dump('Something went wrong :-( ' + e);
        }
    });

    driver();

Implementation of check() and changes to the process driver are trivial:


    function check(obj) {
        if(obj instanceof Error)
            throw obj;
        else
            return obj;
    }

    function proc(processDef) {
        var process = processDef();

        function driver(value) {
            try {
                var pseudoBlockingCall = process.send(value);

                try {
                    pseudoBlockingCall(driver);
                } catch(e) {
                    driver(e)
                }
            } catch(e if e == StopIteration) {

            }

        }

        return driver;
    }

  • The inner try...catch inside the process driver catches exceptions thrown from pseudo-blocking functions, and passes them back to the process as if they were values, so they will be returned to the caller of the pseudo-blocking function.
  • check() receives yield()‘s return value. If it’s an exception, it throw()s it (thereby sending it across the usual exception channel), otherwise just returns the untouched vaule.
  • An extra try...catch in the process driver lets it ignore the StopIteration exception—we won’t go into that here.

The example below, relying on the revised implementation of proc(), shows that exceptions thrown synchronously in a pseudo-blocking call can be caught and exposed within the process:


    function errorProneFunction() {
        return function(driver) {
            throw new Error('Something went wrong!');
            driver('Some other value for correct computation');
        }
    }

    var driver = proc(function() {
        try {
            var result = check(yield(
                errorProneFunction()
            ));
            dump('Success! ' + result);
        } catch(e) {
            dump('Error. :-( ' + e);
        }
    });

    driver();

Exceptions can be thrown by the asynchronous part of pseudo-blocking call, too: just pass them as argument to driver() where you’d normally pass the result of the computation:


    function getUrl(url) {
        return function(driver) {
            var req = new XMLHttpRequest();
            req.open('GET', url, true);
            req.onreadystatechange = function(event) {
                if(req.readyState == 4) {
                    if(req.status != 200)
                        driver(new Error('Non-success HTTP status!'));
                    else
                        driver(req.responseText);
                }
            };
            req.send(null);
        }
    }

Articles in this series:

  1. Part 1: Problem and basic solution
  2. Part 2: Execution dissected
  3. Part 3: Returning values
  4. Part 4: Error handling