ES 5-6-7: From Callbacks to Promises to Generators to Async/await

RDX
3 min readOct 1, 2015

Let’s take a real life use case with the request library.

Step 1: Callback hell — N levels deep

This is how typical NodeJS code would look like. Every function receives a callback function with a commonly used signature:
function(err, response) { }

Step 2: Promises — 1 level deep

Promise libraries take the typical callback function:
function(err, response) { }
And split those arguments into separate then/catch chainable callbacks:
.then(function(response) { }).catch(function(err) { })

You can use Q, Bluebird or any of the innumerable promise libraries. I’ve used bluebird here. You have to first “promisify” the old-style callback library methods.

Note how you have to use “request.getAsync” instead of “request.get”. That’s called “promisification” (line #2) — it converts regular methods into promise-style methods. Also note how it simplifies the place where foo() is invoked as well.

Step 3: Promises+Generators — 0 level flat

Let’s combine the power of promises and ES6 generators.

“foo” is now almost sequential. We reduced the program from 27 lines (callback style) to 23 lines (promises) to 19 lines (promises + generators). It now appears completely flat without any nested functions. Of course, behind the scenes there are still callbacks happening, but WYSIWYG.

Note that the place where we call “foo” still uses promises. We can flatten that as well to use generators, and it becomes a simple try/catch.

function* callerFunction() {
try {
message = yield foo();
console.log("success!", message);
} catch (err) {
console.log("error!", err);
}
}
callerFunction = Promise.coroutine(callerFunction);
callerFunction();

And the place where you call “callerFunction” can also be flattened, and so on, and so on until the topmost entry point of the app. For web applications, that entry point is the web framework. If the web framework is aware of using generators and promises, you can basically make all your functions as generators and forever flat. And you get close to something like koa.

Transforming Tests

Generators are immensely useful in writing Tests (Mocha/etc).Tests usually have a lot of callbacks, but they run in sequence. What a waste of asynchronicity. You can write test cases today using Generators, without worrying about switching testing frameworks.

Step 4: ES7 async/await

ES7 async/await further works on top of generators. Babel already has it in enabled (though its still in staging), so you can try this today.

Wait, how does this work?

This whole magic simply works because NodeJS callbacks have a standard signature of function(err, response) { }.

Promise libraries simply act as a glue between your code, target function (request.get/post/…) and a Deferred object.

deferred = // create a Deferred() object
customCallback = function(err, response) {
if (err) deferred.reject(err);
else deferred.resolve(response);
}
// call original request.get/post/... with customCallback
// return you the Deferred's promise

When ES6 introduced generators, Promise libraries hooked in with a nify hack: If you “yield a promise”, you’ll get the resolved value back.

try {      response          =   yield request.getAsync(...)
^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
to get the response yield a promise
} catch (error) { }
^^^^^^^^^^^^^^^^^^^^^
catch the error

How Generators work is a larger topic altogether. I recommend reading davidwalsh.name/es6-generators or any of the innumerable articles on the web.

--

--

RDX

Simpleton, CI/CD Platformer at goeuro.com, ex-ThoughtWorker, Full stack engineer, Hack Java/Node/Frontend/Ruby/Docker/OSS, Practice XP/KISS/Lean