Exploring Continuations: Resumable Exceptions

June 08, 2016

In the last few posts, I explained the concepts behind Unwinder which implements continuations in JavaScript. We talked about continuations and how to implement a stepping debugger with them.

Now I'd like to experiment with new language constructs that we can build with continuations. The next several posts will explore these ideas. I am naming this series "Exploring Continuations."

Today we're going to implement resumable exceptions. Common Lisp is known for this feature. Few other languages implement them, but I found an implementation for OCaml by (unsurprisingly) Kiselyov.

This is what you can do at the end:

try {
  console.log("hi");
  console.log(raise new Error("hello"));
}
handle(e) {
  console.log("handled");
  resume e with 5;
}

// Output:
// hi
// handled
// 5

I'm not convinced this is particularly useful. It allows you to coordinate code outside of the normal call stack, and I think there are better ways to express this. But let's implement it anyway.

Common Lisp does not use continuations to implement resumable exceptions (called "conditions"), but uses a neat trick to avoid unwinding the stack when they are thrown. We're going to use continuations though, because, heck, I implemented them and we're going to use them.

First, let's implement Try. Remember that this needs to be run in Unwinder which exposes continuations through callCC. There is an online editor here.

Because we can't extend the syntax yet, our try/catch blocks must be implemented as functions. So the usage will look like this:

Try(
  function() {
    console.log('main code');
  },
  function(err) {
    console.log('catch handler');
  }
);

The first function is the main code and the second is the handler (a catch block).

This is the implementation of Try:

var tryStack = [];

function Try(body, handler) {
  var ret = callCC(function(cont) {
    tryStack.push(cont);
    return body();
  });
  tryStack.pop();

  if(ret.__exc) {
    return handler(ret.__exc);
  }
  return ret;
}

If you don't understand continuations or callCC, please my introductory blog post first.

Try gets the current continuation, pushes it on a stack, and calls the code normally. If no exceptions occurred, the return value will by returned from callCC and assigned to ret, and the __exc property will not exist and the value will be returned normally. (We could do more robust type checking for exceptions, but checking for __exc is good enough for now even if it may have false positives.)

The important part is that for the dynamic extent of body, there exists a continuation on the stack which can be used to jump back and call the handler. We use the stack in Throw to implement these semantics.

This is the implementation of Throw:

function Throw(exc) {
  if(tryStack.length > 0) {
    tryStack[tryStack.length - 1]({ __exc: exc });
  }
  // Unhanded exception, so use then normal `throw` to abort
  // the program
  throw exc;
}

First, let's look at what happens with unhanded exceptions. That happens when tryStack is empty, and all we do is use the native throw to stop the program. That's the only purpose of using the native throw.

If there is a continuation on the try stack, we resume the top continuation with the exception value. Remember, calling a continuation aborts the current stack and restores the entire stack from where the continuation is saved. So this will resume the code in Try at the point of assigning a value to ret, and { __exc: exc } will be assigned to it, it pops off the continuations from the stack, and will call then handler with the exception.

With Try/Throw implemented, we can use them to dispatch exceptions. Check out the example usage below and even run it interactively.

// IMPLEMENTATION (read below for usage) var tryStack = []; function Try(body, handler) { var ret = callCC(function(cont) { tryStack.push(cont); return body(); }); tryStack.pop(); if(ret.__exc) { return handler(ret.__exc); } return ret; } function Throw(exc) { if(tryStack.length > 0) { tryStack[tryStack.length - 1]({ __exc: exc }); } throw exc; } // EXAMPLE function times2(x) { console.log('x is', x); if(x < 0) { Throw(new Error("error!")); } return x * 2; } function main(x) { return times2(x); } Try( function() { console.log(main(1)); console.log(main(-1)); }, function(ex) { console.log("caught", ex); } );
Click "Run" to begin a stepping session, or "Run & Ignore Breakpoints" to see the output.

In the above example, when -1 is passed to times2 an exception will be thrown. If you step through the code, you will see that our handler is called and it logs the error. It all works!

If you play with the code, you will see that nested try statements and throwing from catch blocks all work as expected. Here are some more examples:

Try(
  function() {
    Throw("from body");
  },
  function(ex) {
    console.log("caught:", ex);
    Throw("unhandled");
  }
);

// caught: from body
// error unhandled
Throwing from inside a catch block. You will see a globally unhanded error notification.
Try(
  function() {
    Try(
      function() {
        Throw("from body");
      },
      function(exc) {
        console.log("caught:", exc);
        Throw("from inner");
      }
    )
  },
  function(exc) {
    console.log("outer caught:", exc);
  }
);

// caught: from body
// outer caught: from inner
Nested try/catch blocks.

Making Resumable

Now that we have exceptions, what would it take to make them resumable?

It's actually very easy. We just have to save the continuation from where the Throw occurred, and have Resume invoke it.

function Throw(exc) {
  if(tryStack.length > 0) {
    return callCC(function(cont) {
      exc.__cont = cont;
      tryStack[tryStack.length - 1](exc);
    });
  }
  throw exc;
}

function Resume(exc, value) {
  exc.__cont(value);
}

The only thing we added to Throw is the callCC call and exc.__cont = cont to save the continuation. This saves the point of the program when Throw was invoked.

Let's use our previous example, but change the catch handler to resume the exception:

function times2(x) {
  console.log('x is', x);
  if(x < 0) {
    Throw(new Error("error!"));
  }
  return x * 2;
}

function main(x) {
  return times2(x);
}

Try(
  function() {
    console.log(main(1));
    console.log(main(-1));
  },
  function(ex) {
    Resume(ex);
  }
);

What do you think will happen? Try running the code in the online editor.

I'll give another second to guess what the output is.

Ready? Here is the output of the program:

x is 1
2
x is -1
-2

Whoa, how did -2 ever get printed? times2 was supposed to throw an exception on negative numbers. However, instead of logging the exception, our handler resumes it. This means the program continues where Throw was invoked, which in this program returns the number multiplied by 2.

This is starting to feel a little weird. What does it mean to "resume an exception"? In most cases it would be disastrous to do so, as the reason an exception occurred is because the program is in a bad state. Continuing execution surely can't be good.

This is why Common Lisp calls them "conditions". They aren't really exceptions anymore, but a way to "signal" certain behaviors to a dynamic handler. The code invoking Throw has to participate in this discussion: it needs to be written to be resumed. In my opinion, this isn't really about resumable exceptions at all, but a way to coordinate tasks outside of the normal stack.

Because this is more about coordination, I think "resumable exceptions" is a bad way to think about this. It would be better to express this in clearer terms, and we will look at similar (but better) control constructs in future posts.

Fixing the Syntax

Using Try and Resume as methods is bulky. What we really want is to extend the syntax of the language to express this better. sweet.js macros allow you to do this.

First, let's define the language we want. We will still use try to introduce handlers. However, to reduce confusion with native try/catch, we will use handle instead of catch. To throw errors, we will use raise instead of throw (and rename our Throw method to Raise). Lastly, to resume errors, we will use resume <exc> with <expr>. This allows you to write code like this, as seen at the beginning of the post:

try {
  console.log("hi");
  console.log(raise new Error("hello"));
}
handle(e) {
  console.log("handled");
  resume e with 5;
}

// Output:
// hi
// handled
// 5

The macros for these these are very simple. Don't worry about understanding this. If you are interested, you can read the official tutorial. Note: the online editors for Unwinder do not support macros yet, but the compile script does.

syntax try = function(ctx) {
  var body = ctx.next().value.inner();
  var handle = ctx.next().value;
  if(handle.val() !== "handle") {
    return #`try { ${body} } ${handle}`;
  }

  var binding = ctx.next().value;
  var handler = ctx.next().value;
  return #`Try(function() { ${body} },
               function ${binding} ${handler})`;
}

syntax raise = function(ctx) {
  var expr = ctx.next("expr").value;
  return #`Throw(${expr})`;
}

syntax resume = function(ctx) {
  var exc = ctx.next().value;
  // eat `with`
  ctx.next();
  var expr = ctx.next("expr").value;
  return #`Resume(${exc}, ${expr})`;
}
Macros for native try/handle syntax and raising and resuming exceptions.

One Last Example

We haven't showed an example yet that might possibly be used in the real world. Here is one that tries to do so.

This code defines an openFile function that other functions use to open files. openFile will raise an error when a file can't be found, but it allows that error to be resumed. If it is resumed, it uses the new value as the file contents. That means that handlers can "override" file-not-found errors with something else. Note: I'm not saying this is a good idea. We're just playing around.

function OpenFileException(msg, path) {
  this.message = msg;
  this.path = path;
}

function openFile(path, cb) {
  readFileSync(path, (err, contents) => {
    if(err) {
      contents = raise new OpenFileException("file not found", path);
    }
    cb(contents);
  });
}

function processText() {
  openFile("/foo/bar.txt", contents => {
    // do some cRaZy processing of `contents`
  });
}

function main() {
  try {
    processText();
  }
  handle(e) {
    if(e instanceof OpenFileException) {
      ajax(baseUrl + "/fetch" + e.path,
           contents => { resume e with contents });
    }
    raise e;
  }
}

The nice thing here is that processText is completely blind to what's going on. The main function adds a handler that remotely fetches a file if it's not available locally, and processText just uses openFile normally.

Hopefully that makes it clear that this is more about coordination than anything else. This allows openFile and main to coordinate bidirectionally outside of the normal call stack. This might make debugging harder, but I don't see why it would be much harder than generators already are.

The above example is not runnable, and I did not give many runnable examples. Use the times2 program as a starting point, and I encourage you to play with it. We will implement similar control constructs (with more straight-forward APIs) in future posts.

Output:

Stack: