Featured, Javascript

Writing solid asynchronous code using Promises

There are two approaches to handling asynchronous control flow in JavaScript at the moment: Callbacks and Promises. Node.js core APIs, and thus most things written for Node, are callback based. I’m not claiming that callbacks should never used, but I believe Promises simply are a better solution to the same problem. Whereas callbacks are “the simplest solution that would work”, Promises is a thought-out solid solution that, we will see, don’t break your code!

Callbacks

Could not embed GitHub Gist 97692f4823c84afad3bf: API rate limit exceeded for 192.81.221.156. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)

Node.js core APIs uses callbacks to allow your to write code that executes after an I/O operation is either finished or erred. The case for callbacks is that adhering to Node.js APIs brings familiarity for other developers. The function(err, data) pattern will not raise eyebrows for other JavaScript developers. The case against callbacks is that they break the exception system, it brings Callback Hell, it requires you to write your functions wholly differently that you’d do if they weren’t async. Who takes an err as the first argument? And they even break language (any language) standards such as Exceptions!

The Callback Hell argument is weak as there is nothing in the pattern forcing you to write ten levels of callbacks nested as anonymous functions inline, but the pattern does make it easy for you as a developer to do so.

Promises

Promises is a coming ECMAScript 6 specification that’s extracted from open source libraries providing this functional approach to solving control flow for async code. The concept behind promises is that when returning a Promise you are returning an object that promises to either successfully resolve with a value or to reject with a reason.

Thenables

Promises are sometimes called thenables, referring to one of the most important parts of the specification stating that every Promise must have a method then that takes one or two functions as arguments, a resolvedHandler and a rejectedHandler. Every such handler must then return a new Promise — often handled by the library for you as well — thus achieving chaining: promiseMe().then().then().then(resolved).

Some of the important benefits promises brings to the callback-table are that they

… don’t force me to rewrite all of my code

The nice thing about promises is that they don’t interfere with a functions arguments but rather strictly adheres to arguments as input and return values as output. Callbacks are notoriously bad because they conflate inputs and outputs for a function. Promises are also interchangeable so instead of passing a callback 5 levels deeper you can just return the promise in every function — even changing it on the way — before you grab it from the entry point.

Could not embed GitHub Gist 97692f4823c84afad3bf: API rate limit exceeded for 192.81.221.156. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)

It is true that some code at some point has to change to create the promises, but most changes can be kept at wrapping function calls.

… will compose

Promises are composable so you can achieve g(f(x)), an impossible feat using callbacks. The syntax changes so this isn’t perfect either, but supporting square(square(2)) would require more cruft in the square function.

Could not embed GitHub Gist 97692f4823c84afad3bf: API rate limit exceeded for 192.81.221.156. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)

Lets compare this with how we would solve this out of the box with callbacks:

Could not embed GitHub Gist 97692f4823c84afad3bf: API rate limit exceeded for 192.81.221.156. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)

Lets take the Promise composition example and adapt it to use an existing sync square method to prove that we can wrap existing code into promises/thenables:

Could not embed GitHub Gist 97692f4823c84afad3bf: API rate limit exceeded for 192.81.221.156. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)

… exceptions!

Promises correctly bubbles exception upwards, aligning Promise based code with its synchronous counterpart. Look at this callback implementation, it’s obviously convoluted and brittle:

Could not embed GitHub Gist 97692f4823c84afad3bf: API rate limit exceeded for 192.81.221.156. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)

Lets write the same code using Promises:

Could not embed GitHub Gist 97692f4823c84afad3bf: API rate limit exceeded for 192.81.221.156. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)

Promises uses plain normal exceptions for its error handling, meaning we automatically can work with built-ins like JSON.parse in this example.

Pitfalls of Promises

Wrapping node core API

Unfortunately we need to fight Node.js when going pure Promises. It introduces some complexity but promises libraries help us out with wrapper methods:

Could not embed GitHub Gist 97692f4823c84afad3bf: API rate limit exceeded for 192.81.221.156. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)

I’m in a romantic relationship with async.js

You’re in luck! Most things you thought async kicked ass at, Promises pretty much one-ups! Lets take something a little “complex” — the auto example for async.js and the same written using Promises:

Could not embed GitHub Gist 97692f4823c84afad3bf: API rate limit exceeded for 192.81.221.156. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)
Could not embed GitHub Gist 97692f4823c84afad3bf: API rate limit exceeded for 192.81.221.156. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)

Having said so, async.js is still an awesome library with a ton of great control flow helpers.

Promises are slow!

This used to be a valid argument but with the Bluebird Promise library the arguments moot. Gorgi Kosev shared an analysis of async patterns where Bluebird is almost as fast as plain callbacks, and over twice as fast as async.js. The same is the case for memory usage.

Get me some Promises

There’s a lot of Promises implementations popping up, but I will recommend three concrete ones:

  • Q.js — One of the older and more mature libraries. Heavily used and feature rich
  • RSVP.js — Written for Ember by Yehuda Katz
  • Bluebird.js — My favourite library. Unmatched performance

I would also recommend you to learn more from people smarter than me who has written about Promises:

About Raymond Julin

Lead developer at Keyteq Labs, a product business in Bergen, Norway with a reputation for modern user friendly solutions.

5 Comments

  1. This is the first time I have understood ANY advantages of promises over callbacks. Most of the reasons people cite are bogus problems that you can solve by just rearranging code as this author says.

    “The Callback Hell argument is weak as there is nothing in the pattern forcing you to write ten levels of callbacks nested as anonymous functions inline,…”

    exactly. Or, by simply creating named functions and passing the names for each callback argument. THese functions are local to the function they’re declared in; part of its closure.

    inMyClosure = ‘ merry xmas';
    function aaa() {
    setTimeout(bbb, 1000);
    }

    function bbb() {
    setTimeout(ccc, 1000);
    }

    function ccc() {
    inMyClosure = ‘ happy new year';
    }

    setTimeout(aaa, 1000);

  2. josefB

    In Google Chrome version 38, your code example are not showing. If I do view source they are there. Excellent article, btw.

  3. Esa Laitila

    Very good explanation. I have tried to understand Promises/Bluebird, and now I see some light :)

  4. Great article, thank you Raymond.

    Here is a script to get a list of posts from MongoDB. I’m having to use Promise and Async both. Could you suggest any way to handle this logic with Promise only?

    http://www.chopapp.com/#h8xcb3lx

    Thanks.

  5. Chris

    The gists aren’t populating.

Leave a Reply to josefB Cancel reply