Exploring Continuations: Resumable Exceptions
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.
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
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
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})`;
}
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.