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
andTHROW
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 theExecute
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.