Error handling

In the code on the previous page I discarded the if (err) throw new Error(err) lines. “Handling” errors in such a way is meaningless, because it forbids error recovery. We just throw, and the program aborts (unless you setup a global catch-all handler, but being global, it won't have enough context to nicely recover from the error).

However, if you did anything serious in NodeJS you know that those lines are really necessary, in every callback. Except that, instead of throwing, it's a better idea to just pass the error to the previous callback (or handle it and recover if appropriate). The convention in NodeJS is that a callback will take an error as first argument, and the result as second. So we have to insert this line in every callback, right at the beginning: if (err) return callback(err). That's crude, to say the least.

Exceptions in λanguage

Thanks to explicit continuations, in λanguage we can implement an exception system that will work across asynchronous calls.

We will implement a rudimentary system here—two primitives, TRY and THROW (uppercase, to avoid clashing with JavaScript's own try and throw keywords). Here's possible usage:

TRY(
  λ() {
    let (data = readFile("/tmp/notexists")) {
      print("File contents:");
      print(data);
    }
  }

  ## "ENOENT" means file not found
  , "ENOENT", λ(info){
    print("File not found:", info);
  }

  ## generic handler
  , true, λ(info, code){
    print("Some error.  Code:", code, "info:", info);
  }
);

It looks ugly because we don't extend the parser with new syntax—TRY and THROW are just plain functions. But if we wanted anything fancier, we could, of course, modify the parser.

If you'd like to test this example, download the exception-enabled primitives.

TRY receives a function and a bunch of error handlers. Each error handler must be preceded by the error code (so the number of arguments should be always odd). If true is passed instead of an error code, then that handler will be called for any error if not handled before. For the above to work, the primitive has been modified to call THROW (instead of JS's throw) when an error occurred:

readFile = function(k, filename) {
  fs.readFile(filename, function(err, data){
    if (err) return Execute(THROW, [ k, err.code, filename ]);
    Execute(k, [ data ]);
  });
};

It has to call THROW through the Execute loop, of course.

You can play with it in your browser:

Note that TRY, being like an ordinary function, returns a value. Its result is the lambda's result if no error was thrown, or the result of the matching error handler, so you could write for example:

data = TRY(λ(){
             readFile("/foo/bar.data");
           },
           "ENOENT", λ(){
             "default data"; # in case file was not found
           });

The beauty of our system is that it allows us to care about the error only in the places we want, rather than at every function call (as NodeJS requires). I mean, it's just like try/catch. For instance, our copyTreeSeq function does not have to care about errors, because the caller can intercept them by wrapping the call in a TRY:

copied = TRY(
  λ(){ copyTreeSeq(source, destination); true },
  ## generic error handler
  true, λ(code, info){ print("Some error occurred", code, info); false }
);
## .. our program resumes from here either way.
##    `copied` will be true if the operation was successful.

In order to know if the operation was successful or not with plain NodeJS, you actually have to include if (err) return callback(err) at the start of every callback. It's not possible to abstract that out by wrapping over the fs API.

The exception system we implement here will fall short in the face of parallelism. Proper support for the parallel copyTree needs some more thinking…

Implementation

Here's the implementation of TRY and THROW as primitives:

var exhandlers = [];

window.THROW = function(discarded, code, info) {
  while (exhandlers.length > 0) {
    var frame = exhandlers.pop();
    for (var i = 0; i < frame.length; ++i) {
      var x = frame[i];
      if (x.code === true || x.code === code) {
        x.handler(x.continuation, info, code);
        return;
      }
    }
  }
  throw new Error('No error handler for [' + code + '] ' + info);
};

window.TRY = function(k, f) {
  var frame = [];
  for (var i = 2; i < arguments.length;) {
    var x = {
      code: arguments[i++],
      handler: arguments[i++],
      continuation: k
    };
    if (typeof x.handler != 'function')
      throw new Error('Exception handler must be a function!');
    frame.push(x);
  }
  exhandlers.push(frame);
  f(function(result){
    exhandlers.pop();
    k(result);
  });
};

The implementation is simple, and sound if we don't expose a primitive like CallCC. We maintain a stack of frames in exhandlers. TRY will push a new frame, containing the new set of error handlers. THROW pops a frame and executes the first handler that matches. The continuation it passes to it (x.continuation) is the continuation of the TRY block where that particular handler was introduced—therefore THROW does not return, but jumps to some location previously set (just like exceptions in most programming languages). When no more frames are available, THROW will abort the program by throw-ing a hard JS error.

Incidentally, this should explain why exceptions are generally expensive. Every programming language, no matter how heavily optimized, will have to do something like the above on entering a try block.

The END

I hope this was worth your time.
I appreciate any feedback.

Welcome! (login)