Using promises

Most people are consumers of promises returned from functions. A returned promise is not a control surface, rather merely a representation of the eventual completion (or failure) of the asynchronous operation launched by the function.

Essentially, a promise is a returned object you attach callbacks to, instead of passing callbacks into a function. E.g. instead of a function taking two callbacks:

doSomething(successCallback, failureCallback);

modern functions return a promise you can attach your callbacks to instead:

let promise = doSomething();
promise.then(successCallback, failureCallback);

or simply:

doSomething().then(successCallback, failureCallback);

We call this an asynchronous function call. This convention has several advantages. The first is chaining.

Chaining

Here's the magic: the then function returns a new promise, different from the original:

let promise = doSomething();
let promise2 = promise.then(successCallback, failureCallback);

or

let promise2 = doSomething().then(successCallback, failureCallback);

This second promise represents the completion not just of doSomething(), but also of the successCallback or failureCallback you passed in, which can be other asynchronous functions returning a promise. When that's the case, any callbacks added to promise2 get queued behind the promise returned by either successCallback or failureCallback.

Basically, each promise represents the completion of another asynchronous step in the chain.

In the old days, doing several asynchronous operations in a row would lead to the classic callback pyramid of doom:

doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

With modern functions, we attach our callbacks to the returned promises instead, forming a promise chain:

doSomething().then(function(result) {
  return doSomethingElse(result);
})
.then(function(newResult) {
  return doThirdThing(newResult);
})
.then(function(finalResult) {
  console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);

The arguments to then are optional, and catch(failureCallback) is short for then(null, failureCallback). You might see this expressed with arrow functions instead:

doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
  console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);

Important: Always return promises up, otherwise callbacks won't chain, and errors won't be caught.

Error propagation

You might recall seeing failureCallback three times in the pyramid of doom earlier, compared to only once at the end of the promise chain:

doSomething()
.then(result => doSomethingElse(value))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log('Got the final result: ' + finalResult))
.catch(failureCallback);

Basically, a promise chain stops if there's an exception, looking down the chain for catch handlers instead. This is very much modeled after how synchronous code works:

try {
  let result = syncDoSomething();
  let newResult = syncDoSomethingElse(result);
  let finalResult = syncDoThirdThing(newResult);
  console.log('Got the final result: ' + finalResult);
} catch(error) {
  failureCallback(error);
}

This symmetry with synchronous code culminates in the async/await syntactic sugar in ECMAScript 2017:

async function foo() {
  try {
    let result = await doSomething();
    let newResult = await doSomethingElse(result);
    let finalResult = await doThirdThing(newResult);
    console.log('Got the final result: ' + finalResult);
  } catch(error) {
    failureCallback(error);
  }
}

It builds on promises, e.g. doSomething() is the same function as before. You can read more about the syntax here.

Promises solve a fundamental flaw with the callback pyramid of doom, by catching all errors, even thrown exceptions and programming errors. This is essential for functional composition of asynchronous operations.

Wrapping callback APIs

A Promise can be created from scratch using its constructor, but in an ideal world you would never need to. Alas, some APIs still expect success and/or failure callbacks to be passed in the old way. The quintessential example is the setTimeout() method:

setTimeout(() => saySomething("10 seconds passed"), 10000);

Mixing old-style callbacks and promises is problematic. If saySomething fails or contains a programming error, nothing catches it.

Therefore, best practice is to wrap such functions at the lowest possible level, and then never call them directly again:

let wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);

Basically, the promise constructor takes an executor function that lets us resolve or reject a promise manually. Since setTimeout doesn't really fail, we left out reject in this case.

Composition

Promise.resolve() and Promise.reject() are shortcuts to manually create an already resolved or rejected promise respectively. This can be useful at times.

Promise.all() and Promise.race() are two composition tools for running asynchronous operations in parallel.

Sequential composition is possible using some clever JavaScript:

[func1, func2].reduce((p, f) => p.then(f), Promise.resolve());

Basically, we reduce an array of asynchronous functions down to a promise chain equivalent to: Promise.resolve().then(func1).then(func2);

Timing

To avoid surprise, functions passed to then will never be called synchronously, even with an already-resolved promise:

Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2

Instead of running immediately, the passed-in function is put on a microtask queue, which means it runs later when the queue is emptied at the end of the current run of the JavaScript event loop. I.e. pretty soon:

var wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait().then(() => console.log(4));
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
console.log(1); // 1, 2, 3, 4

See also

Document Tags and Contributors

 Contributors to this page: Jib, Sheppy
 Last updated by: Jib,