Skip to content

Instantly share code, notes, and snippets.

@jclem
Last active December 25, 2015 05:59
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jclem/6929172 to your computer and use it in GitHub Desktop.
Save jclem/6929172 to your computer and use it in GitHub Desktop.
An attempt at explaining promises.

promises

Before Promises

Say in a JavaScript application, we need to make a request to http://www.example.com and then log the response. Let's walk through a few ways to do it.

Here's the worst way:

var result;

// httpGET, hypothetically, does the request and
// sets result to the value of the response
httpGET('http://www.example.com', result);

var wait = setInterval(function() {
  if (result !== undefined) {
    clearInterval(wait);
    console.log(result);
  }
}, 0);

Given a function that gets a URL and updates a passed-in variable with the response, we can wait in a non-blocking manner (setInterval, even when passed 0, never blocks) until the variable is no longer undefined.

Here's the most common way:

httpGET('http://www.example.com', function (result) {
  console.log(result);
});

A function gets a URL, and then calls a function argument it's passed, giving that function the result of the HTTP request as an argument.

This is the type of programming that typically leads to "Callback Hell", when a program needs to perform a series of tasks, some of which may be non-blocking, which depend on the result of a previous task.

Frequently, callback hell can be avoided simply by passing around function names rather then function literals:

httpGET('http://www.example.com', handleResult);

function handleResult (result) {
  console.log(result);
}

For the Node.js community at the moment, they consider the problem to be solved—it's up to the programmer to avoid callback hell by, well, avoiding callback hell. To be clear, the Node.js convention goes like this:

httpGET('http://www.example.com', handleResult);

// By convention, async functions (like the
// hypothetical httpGET) pass an error (if
// applicable) and the result (if there's
// no error) into the given callback function.
function handleResult (err, result) {
  if (err) {
    return handleErr(err);
  }
  
  console.log(result);
}

Enter Promises

From the Promises/A spec:

A promise represents the eventual value returned from the single completion of an operation. A promise may be in one of the three states, unfulfilled, fulfilled, and failed.

A promise must implement a single function, then, that accepts two (onProgress below is optional) functions as arguments:

// httpGET now returns a promise
var promise = httpGET('http://www.example.com');
promise.then(onFulfilled, onError, onProgress);

More concisely written as:

httpGET('http://www.example.com').then(onFulfilled, onError, onProgress);

When httpGET is successful, it calls only onFulfilled and passes in only its successful result value as an argument. If the request fails, it calls onError, passing in only an error. Optionally, it could implement an onProgress handler, sending updates periodically to this function

Note that a promise can only move from unfulfilled to fulfilled, or unfulfilled to failed. It can't become failed and then fulfilled, or unfulfilled, etc.

That's part 1, the most basic part of the spec. Part 2 is a little more frequently misunderstood. Again, from the Promises/A spec:

This function [(then)] should return a new promise that is fulfilled when the given fulfilledHandler or errorHandler callback is finished. [...] The value returned from the callback handler is the fulfillment value for the returned promise.

This is what allows promises to chain together. In the following example, each callback function also returns a promise (httpGET returns a promise), so the next callback function passed to the next then call is only executed if the previous one is successful:

var results = []

httpGET('http://www.example.com').then(function (result) {
  
  results.push(result);
  return httpGET('http://www.example.com/');

}).then(function (result) {

  results.push(result);
  return httpGET('http://www.example.com/');

}).then(function (result) {

  results.push(result);
  console.log('All Done! 3 sequential calls to http://www.example.com');
  console.log('The results:')
  
  for var i = 0; i < results.length; i++ {
    console.log(results[i]);
  }

});

(Maybe this does look like callback hell to you, but it's only a single level of nesting. Even that could be avoided by pulling those function literals out and just passing around function names).

Another important detail is that if one of the success handlers throws an error (or returns a promise that's rejected), the promise returned by the outer then call is failed. This means that for a series of promises, error handling might only need to be done once:

var results = []

httpGET('http://www.example.com').then(function (result) {
  
  results.push(result);
  return httpGET('http://www.example.com/');

}).then(function (result) {

  // Remember that this is the first argument
  // to `then`, so it's only called if the
  // previous callback's returned promise
  // is resolved.
  results.push(result);
  return httpGET('http://www.example.com/');

}).then(allDone, onError);

function allDone (result) {
  // Only called if there are no errors.
  results.push(result);
  console.log('All Done! 3 sequential calls to http://www.example.com');
  console.log('The results:')
  
  for var i = 0; i < results.length; i++ {
    console.log(results[i]);
  }
}

// This will log an error when
// the first httpGET fails (if
// failure occurs)
function onError (err) {
  console.log(err);
}

DRYing the above example up a little:

var results = [];

httpGET('http://www.example.com')
  .then(getAgain)
  .then(getAgain)
  .then(allDone, onError);

function getAgain (result) {
  results.push(result);
  return httpGET('http://www.example.com');
}

function allDone (result) {
  results.push(result);
  
  console.log('All Done! 3 sequential calls to http://www.example.com');
  console.log('The results:')
  
  for var i = 0; i < results.length; i++ {
    console.log(results[i]);
  }
}

function onError (err) {
  console.log(err);
}

(Note that if a success callback function returns any value that's not a promise, it's treated like a promise that's immediately resolved.)

As I see it, the advantage that promises provide over the more common Node.js-style callbacks is that they provide a way of writing asynchronous code that remains almost as readable as code that is not asynchronous. There have been many times when I've read through code, chasing around functions and the functions that they called in return when successful.

After I finished writing this, I found this article, which is a pretty good explanation. FWIW, my personal favorite implementation is q, a library that implements the Promises/A spec and then adds some really nice stuff onto it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment