asyncawait v1.0async in Depth: Suspendable Functionsthis Contextasync.mod Functionawait in Depth: Awaitable Expressionsawait?awaitasyncawait addresses the problem of callback hell in Node.js JavaScript code. Inspired by C#'s async/await feature, asyncawait enables you to write functions that appear to block at each asynchronous operation, waiting for the results before continuing with the following statement. For example, you can write the following in plain JavaScript:
var foo = async (function() {
var resultA = await (firstAsyncCall());
var resultB = await (secondAsyncCallUsing(resultA));
var resultC = await (thirdAsyncCallUsing(resultB));
return doSomethingWith(resultC);
});
which, with one proviso, is semantically equivalent to:
function foo2(callback) {
firstAsyncCall(function (err, resultA) {
if (err) { callback(err); return; }
secondAsyncCallUsing(resultA, function (err, resultB) {
if (err) { callback(err); return; }
thirdAsyncCallUsing(resultB, function (err, resultC) {
if (err) {
callback(err);
} else {
callback(null, doSomethingWith(resultC));
}
});
});
});
}
The function foo does not block Node's event loop, despite its synchronous appearance. Execution within foo is suspended during each of its three asynchronous operations, but Node's event loop can execute other code whilst those operations are pending. You can write code like the above example in a HTTP request handler, and achieve high throughput with many simultaneous connections, just like with callback-based asynchronous handlers.
In short, asyncawait marries the high concurrency of asynchronous code with the visual clarity and conciseness of synchronous code. Rather than passing callbacks and error-backs, you can return values and use try/catch blocks. Rather than requireing specialised asynchronous control-flow constructs like each and whilst, you can use plain JavaScript constructs like for and while loops.
Like co, asyncawait can suspend a running function without blocking Node's event loop. Both libraries are built on coroutines, but use different technologies. co uses ES6 generators, which work in Node >= v0.11.2 (with the --harmony flag), and will hopefully be supported someday by all popular JavaScript environments and toolchains.
asyncawait uses node-fibers. It works with plain ES3/ES5 JavaScript, which is great if your tools do not yet support ES6 generators. This may be an important consideration when using compile-to-JavaScript languages, such as TypeScript or CoffeeScript.
A similar outcome may be achieved by transforming JavaScript source code in a preprocessing step. streamline.js is an example of this method. Code using asyncawait is executed normally without any code tranformation or preprocessing.
asyncawait represents one of several viable approaches to writing complex asynchronous code in Node.js, with its own particular trade-offs. Notable alternatives include async, bluebird and co, each with their own trade-offs. The following table summarises some of the alternatives and their pros and cons. For more information about how the alternatives compare, take a look in the comparison folder.
asyncawait may be a good choice if (a) you need highly concurrent throughput, (b) your asynchronous code must be clear and concise, (c) your code targets Node.js, and (d) you are limited to ES3/ES5 syntax (e.g. you write in TypeScript or CoffeeScript).
| Max. throughput (full event loop utilisation) | Concise, clear code (control-flow, data-flow and error-flow) | Max. support for Node.js dev/build tools | Max. support for JS envs (eg Node + browsers) | |
|---|---|---|---|---|
| Plain synchronous code | :heavy_exclamation_mark:[1] | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Plain callbacks | :white_check_mark: | :heavy_exclamation_mark:[2] | :white_check_mark: | :white_check_mark: |
Callbacks + control-flow (e.g. async) |
:white_check_mark: | :heavy_exclamation_mark:[3] | :white_check_mark: | :white_check_mark: |
Promises + control-flow (e.g. bluebird) |
:white_check_mark: | :heavy_exclamation_mark:[3] | :white_check_mark: | :white_check_mark: |
Coroutines with co |
:white_check_mark: | :white_check_mark: | :heavy_exclamation_mark:[4] | :heavy_exclamation_mark:[5] |
Coroutines with asyncawait |
:white_check_mark: | :white_check_mark: | :white_check_mark: | :heavy_exclamation_mark:[6] |
Footnotes:
[1] Each synchronous call blocks Node's event loop. All concurrent tasks are blocked, and the event loop sits idle, until the call completes.
[2] Plain callbacks rapidly become unwieldy for complex asynchronous tasks. See comparison.
[3] Whilst better than plain callbacks, these styles still produce longer and more complex code than synchronous or coroutine-based code. See comparison.
[4] Some tools do not (yet) support ES6 generators, including compile-to-JavaScript languages such as TypeScript and CoffeeScript.
[5] ES6 still has patchy browser support.
[6] Strictly limited to Node.js environments (i.e. no browsers) due to the use of node-fibers.
How well does asyncawait perform? The answer depends on what kinds of performance you care about. As a rough guide, compared with bare callbacks, expect your code to be 70% shorter with 66% less indents and run at 79% of the speed of bare callbacks. OK, so don't trust those numbers (which actually are real) but do check out the code in the comparison folder, and do run your own benchmarks.
npm install asyncawait
asyncawait provides just two functions: async() and await(). You can reference these functions with the code:
var async = require('asyncawait/async');
var await = require('asyncawait/await');
Use async to declare a suspendable function. Inside a suspendable function, use await to suspend execution until an awaitable expression produces its result. Awaitable expressions typically involve performing asynchronous operations.
Note the spacing after async and await in the examples. They are just plain functions, but the space makes them look more like keywords. Alternatively if you really want them to stand out, you could use names like __await__ or AWAIT, or whatever works for you.
var async = require('asyncawait/async');
var await = require('asyncawait/await');
var Promise = require('bluebird');
var fs = Promise.promisifyAll(require('fs')); // adds Async() versions that return promises
var path = require('path');
var _ = require('lodash');
/** Returns the number of files in the given directory. */
var countFiles = async (function (dir) {
var files = await (fs.readdirAsync(dir));
var paths = _.map(files, function (file) { return path.join(dir, file); });
var stats = await (_.map(paths, function (path) { return fs.statAsync(path); })); // parallel!
return _.filter(stats, function (stat) { return stat.isFile(); }).length;
});
// Give it a spin
countFiles(__dirname)
.then (function (num) { console.log('There are ' + num + ' files in ' + __dirname); })
.catch(function (err) { console.log('Something went wrong: ' + err); });
The function countFiles returns the number of files in a given directory. To find this number, it must perform multiple asynchronous operations (using fs.readdir and fs.stat). countFiles is declared as a suspendable function by wrapping its definition inside async(...). When countFiles is called with a dir string, it begins executing asynchronously and immediately returns a promise of a result. Internally, countFiles appears to have synchronous control flow. Each await call suspends execution until its argument produces a result, which then becomes the return value of the await call.
The examples folder contains more examples. The comparison folder also contains several examples, each coded in six different styles (using plain callbacks, using synchronous-only code, using the async library, using the bluebird library, using the co library, and using this asyncawait library).
async in Depth: Suspendable FunctionsThe subsections below refer to the following code:
var suspendable = async (function defn(a, b) {
assert(...) // may throw
var result = await (...)
return result;
});
var suspendable2 = async.cps (function defn(a, b) {...});
var suspendable3 = async.thunk (function defn(a, b) {...});
var suspendable4 = async.result (function defn(a, b) {...});
Suspendable functions may accept arguments. Calling suspendable(1, 2) will in turn call defn(1, 2). Suspendable functions may be variadic. They report the same arity as their definition (i.e. suspendable.length and defn.length both return 2).
A suspendable function's definition may return with or without a value, or it may throw. Returning without a value is equivalent to returning undefined. The return value of the definition function becomes the result of the suspendable function (see Obtaining Results from Suspendable Functions).
A suspendable function's definition may throw exceptions directly or indirectly. If any of the await calls in defn asynchronously
$ claude mcp add asyncawait \
-- python -m otcore.mcp_server <graph>