-
Notifications
You must be signed in to change notification settings - Fork 51
Interoperability Details
As mentioned in the README, streamline interoperates seamlessly with standard node.js code: you can call node.js APIs from streamline code, and you can call functions written with streamline from normal node.js code.
There is just one area that deserves a bit of attention: exception handling. Streamline gives you two options:
- a safe option for those who don't want to worry about the details and are ready to pay a slight performance hit.
- a fast option for those who care about details and want optimal code.
The safe option is enabled by adding a line like the following in your streamline source module: // pragma streamline.extraTryCatch
With this option, streamline will add try/catch logic in the generated code to guarantee the following:
- The generated function never throws an exception to its caller. It transforms all exceptions into
callback(err)calls. - If an exception occurs during a callback, it will be properly caught and propagated.
The fast option is the default one. With this option, streamline does not add any try/catch logic to the generated code. So you get optimal performance. You can still do rigorous exception handling but you need a bit of extra care. To explain it I'll start with a small diagram of the code flows inside a chain of asynchronous functions:
f1 begin --> f2 begin --> f3 begin --> f4 begin
f1 callback <-- f2 callback <-- f3 callback <-- f4 callback
And we have to deal with two types of exceptions:
- exceptions that are thrown explictly by a
throwstatement. - exceptions that are thrown implicitly by a bug (dereferencing null for example).
Let us assume that f1 is a hand-written function, that f2 and f3 have been generated by streamline and that f4 is a low level node.js API.
Inside f2 and f3, all the throw statements (in the begin flow and callback flows) are converted into _(err) calls. So, they will reach f1's callback as you would expect them to. But that's all. All other exceptions (thrown by f1 or f4, or caused by bugs in f2 or f3) will propagate as exceptions. In the begin flow, they will bubble up to f1 and in the callback flow they will bubble up to f4. So, we need a bit of extra care at both ends to properly deal with this situation.
Streamline has a built-in helper function for the f4 side: __wrapOut. Instead of calling f4 as f4(args, _), you can call it as:
f4(args, __wrapOut(_))
The __wrapOut helper will set up the try/catch logic which is necessary to make sure that exceptions thrown by the callback flow are properly propagated as _(err) calls.
The f1 side is more tricky. First, you may choose to let the exception bubble up to f1's caller. In this case, all you have to do is test for a non null err parameter in the callback and pass it to the caller's callback.
But if you want f1 to catch and properly handle all exceptions, you have to write code like:
function f1(args, callback) {
try {
// some code here...
f2(function(err, result) {
if (err) return handleError(err);
try {
// continue with result ...
}
catch (ex) {
handleError(ex);
}
});
}
catch (ex) {
handleError(ex);
}
}
The outer try/catch handles exceptions thrown by the begin flow. Nothing new here.
The inner try/catch handles exception thrown by the callback flow. It is very important if f2 or f3 contain try/catch/finally statements that have been transformed by streamline. Without the inner try/catch, exceptions thrown by f1's callback would bubble to the exception handlers contained in f2 and f3 and you would get very disturbing behaviors in this case (catch or finally blocks being executed completely out of sequence).
It is also very important that the handleError function be safe so that no exceptions will ever bubble up to f2 or f3's callbacks. This function should typically be written as follows:
function handleError(ex) {
try {
// handle the exception
}
catch (ex2) {
console.log("UNCAUGHT EXCEPTION: " + ex2); // don't do anything too fancy here
}
}
This may sound complex but it is not really. To summarize, you have to follow some simple rules:
- At the low level, pass
__wrapOut(_)instead of_when calling node APIs. - At the high level, catch the exceptions both in the
beginandcallbackflows. - Don't forget to test the
errparameter in all your callbacks. - Use safe exception handlers that never throw exceptions.
The important point is that if most of your async code is written with streamline, you only need to worry about these issues at the very low level (when calling node or third party APIs) and at the very high level (usually the event dispatcher that calls into your streamlined logic). In the middle, you can write all your code as if it were synchronous. If you need to do some exception handling in the middle, you can do it with standard try/catch/finally statements. Streamline will transform these statements so that they handle the two flows (begin and callback) correctly and you can reason about your exception handling logic without having to worry about the asynchronous nature of the code.
Streamline provides a built-in __wrapOut wrapper to handle APIs that follow the node conventions. If you need to call non standard APIs that use a different callback signature, you can provide your own wrappers. There is one gotcha, though: you have to follow a special naming convention and call these wrappers __wrapXxx because streamline needs to handle them in a special way.