I had to give a little talk about ES6 promises a while back and was forced to organize my thoughts and learn a few things so I thought I might as well share the wealth. This is a progressive introduction to promises starting from the previous status quo (callbacks), then talking about basic usage, tips and tricks, and async/await.

This guide sort of assumes you already know how callbacks work and does not explain them very well.

Why Promises?Why Promises?

Why Promises?

Promises are meant to be an easy API for handling async operations such as:

  • Network requests
  • Database queries
  • User input

Basically, when you have some code that should only run when some other code has finished, and maybe needs some data from that previous code.

Promises replace a callback pattern, which most people find clunkier in many circumstances. Look at the sequential and parallel promises sections below to see some specifics.

Promise Basics: then(), catch(), finally()

Handle Success With then()

Before promises, async operations were usually handled with a callback pattern, which looks like this:

// Callback pattern
getSomethingThatTakesAWhile((result) => {
  console.log(result);
});

where the function getSomethingThatTakesAWhile takes a while to get some data (from the internet, or a database, or something), and when it's done, it now has some data, and it runs the unnamed function passed to it as an argument, passing the new data into it:

(result) => {
  console.log(result);
}

The function can of course do something more interesting with the result than console.log() it but let's just try to keep the example simple.

A promise that did the same thing would look like this:

// Promise pattern
getSomethingThatTakesAWhile()
  .then(result => {
    console.log(result);
  });

This getSomethingThatTakesAWhile() is a function that returns a promise. A promise object has then(), catch(), and finally() methods.

The then(successFunction) method runs successFunction (whatever named or anonymous function you put in there) whenever getSomethingThatTakesAWhile() has finished its work (in this case, has finished getting something).

Handle Errors With catch()

In the callback pattern, errors are usually handled something like this:

// Callback pattern: basic error handling
getSomethingThatTakesAWhile(
  (result) => { console.log(result); },
  (err) => { console.error(err); },
);

where you pass getSomethingThatTakesAWhile two callbacks, the first one does whatever you want with the successful result if it succeeds, the second one does whatever you want with the error if getSomethingThatTakesAWhile spits out an error.

With promises it would look like this:

// Promise pattern: basic error handling
getSomethingThatTakesAWhile()
  .then(result => {
    console.log(result);
  })
  .catch(err => {
    console.error(err);
  });

Handle Whatever Kind Of Resolution With finally()

The last bit of basic promise usage is finally() which executes code when the promise is done, whether it was successful or not. A good use for this might be to stop a loading spinner you started before launching the long task, because whether you failed or succeeded at getting the data, it's certainly not "loading" anymore.

startTheLoadingSpinner();
getSomethingThatTakesAWhile()
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error('Something went wrong, specifically: ' + error);
  })
  .finally(() => {
    stopTheLoadingSpinner();
  });

Multiple Promises: Sequential and Parallel

Sequential Tasks (Chaining)

Sometimes you want to run one task, and only when it's done, run the next one. Maybe the second one needs data from the first one. Maybe you want to get your user ID number from the server, and then use that ID to query some other API to get your profile data. That code would look something like this:

getAnIdFromTheServer()
  .then(id => {
    return getAProfileMatchingThisId(id);
  })
  .then(profile => {
    // make it pretty
    const formattedProfile = formatProfile(profile); 
    console.log(formattedProfile);
  });

where getAnIdFromTheServer() and getAProfileMatchingThisId() are both functions that return promises.

Chaining using the callback pattern often created very difficult-to-read code with many nested callbacks, known colloquially as "callback hell".

// callback hell
doThing(result1 =>
  doThing2(result1, result2 =>
    doThing3(result2, result3 =>
      doThing4(result3, result4 =>
        doThing5(result4, result5 =>
          doThing6(result5)
        )
      )
    )
  )
);

Chained promises avoid this kind of nesting.

// promise chain
doThing()
    .then(result1 => doThing2(result1))
    .then(result2 => doThing3(result2))
    .then(result3 => doThing4(result3))
    .then(result4 => doThing5(result4))
    .then(result5 => doThing6(result5))

A common mistake is to return to the callback hell that promises should have set you free from, by nesting promises unnecessarily, as in this example:

getAnIdFromTheServer()
  .then(id => {
    getAProfileMatchingThisId(id)
      .then(profile => {
        // make it pretty
        const formattedProfile = formatProfile(profile);
        console.log(formattedProfile);
      });
  });

The problem is tacking on a then() within a then():

getAnIdFromTheServer()
  .then(id => {
    getAProfileMatchingThisId(id) // adding a then (nesting)
      .then(profile => {
```javascript
where the better way would be to *return* the profile-fetching promise:
```getAnIdFromTheServer()
  .then(id => {
    return getAProfileMatchingThisId(id);  // return
  })
  .then(profile => { // another top-level then

and let an additional then() (at the same level as the first one) pick it up.

Manipulating Promises

Some important notes before going forward:

You can assign promises to variables.

This makes it easier to put promise code just where you need it. Make sure not to confused functions that return promises with promises themselves!

const doSomethingPromise = doSomethingThatTakesAWhile();

// doSomethingThatTakesAWhile() is a function that
// returns a promise.

// doSomethingPromise is a promise.

doSomethingPromise.then(result => console.log(result));

then(), catch(), and finally() all return promises

This is what makes chaining possible. But if you have the promise stored in a variable, you can call these methods on it any time, you don't have to do it right where the promise is created.

const idPromise = getAnIdFromTheServer();

// idPromise.then() returns a promise
const profilePromise = idPromise
  .then(id => getAProfileMatchingThisId(id));

profilePromise.then(profile => console.log(profile));

Parallel Promises

Sometimes you want to run tasks in parallel. For example, you get your user ID from the server, and then you want to use that ID to fetch profile data and a picture from two different APIs. Both of these tasks need to wait for the ID to come back, but they don't need to wait for each other.

Chaining these 3 tasks in a row would be pretty inefficient. Instead:

const idPromise = getAnIdFromTheServer();

// Both wait for idPromise to finish
// but don't wait for each other.
const profilePromise = idPromise
    .then(id => getAProfileMatchingThisId(id));
const picturePromise = idPromise
    .then(id => getAPictureMatchingThisId(id));

Putting each of these calls in idPromise.then()s means they wait for idPromise to complete before starting, but do not wait for each other.

Suppose you want to wait for both of these things to finish before presenting them to the user (again, you will probably present them in a fancier way than console.log()).

In order to do this, we use Promise.all(). It takes an array of promises, and the function you put in its then() will execute when all of those promises are done. It looks like this:

const profilePromise = idPromise
    .then(id => getAProfileMatchingThisId(id));
const picturePromise = idPromise
    .then(id => getAPictureMatchingThisId(id));

Promise.all([profilePromise, picturePromise])
  .then(([profile, picture]) => {
    console.log(profile, picture);
  });

The console.log(profile, picture) only happens when both profilePromise and picturePromise are done.

The values returned by the promises (profile and picture) are in an array passed to that function as a parameter.

Error handling is pretty handy with Promise.all():

Promise.all([profilePromise, picturePromise])
  .then(([profile, picture]) => {
    console.log(profile, picture);
  })
  .catch(error => {
    // This will catch an error in profilePromise or
    // picturePromise, whichever happens first.
    console.error(error);
  });

It will catch the first error that happens in any of the promises.

Creating Promises

When you create a new promise object, you can provide it with a resolve() function and a reject() function. The resolve() function is what should happen on success, triggering the promise's then() and passing it whatever you passed in resolve().

const myPromise = new Promise((resolve, reject) => {
  resolve('hi');
});
myPromise.then(result => console.log(result)); // logs 'hi'

This example is not very useful because we haven't actually done any asynchronous tasks that take some time to finish. Suppose you do have an asynchronous function, and it has a callback pattern, and you want to turn it into a promise. You might do it like this:

const myPromise = new Promise((resolve, reject) => {
  aFunctionThatHasACallback((callbackResult) => {
    resolve(callbackResult);
  });
});

myPromise.then(result => console.log(result));

Inside the callback, you call resolve(), returning the result through the promise where you can access it with a then().

The reject() is for handling errors, and triggering the promise's catch(). Callbacks usually provide errors as an additional param, so:

const myPromise = new Promise((resolve, reject) => {
  aFunctionThatHasACallback((callbackResult, callbackError) => {
    if (callbackError) {
      reject(callbackError);
    }
    resolve(callbackResult);
  });
});

myPromise
  .then(result => console.log(result))
  .catch(err => console.error(err));

We don't usually want to create a specific promise every time we need to call the callback, so we might as well wrap the callback function and turn it into a general-purpose promise-returning function, just like the getSomethingThatTakesAWhile() examples above.

If you have a callback-pattern function for doing network fetches from some given url, like this:

callbackNetworkFetch(url, (data) => {
  console.log(data);
});

You can wrap it so you can call the same functionality in a promise pattern, like this.

function promiseNetworkFetch(url) {
  return new Promise((resolve, reject) => {
    callbackNetworkFetch(url, (data) => {
      resolve(data);
    });
  });
}

Async/Await

Promises are a lot better than callbacks but I guess they still weren't good enough for some people. The async/await syntax allows you to write promises like synchronous code.

It is somewhat newish so you need to make sure your environment allows it. In Node this means Node 8+. In the browser, it may require polyfills or transpilers (e.g. Babel).

Basic Rules

  • await must always be used inside a function defined with the async keyword. (So it can't be used at the top level.)
  • Async functions always return a promise. If you don't specify a return, the value the promise contains will be empty.

Basic Usage

Here's an example that tries to hit some API endpoint and create a user profile for a given userId.

async function(userId) {
  try {
    await createProfileForUser(userId);
  } catch (error) {
    console.error(error);
  }
  // Async functions automatically return a promise.  But it will be empty if
  // you don't specify what value to return.  Here it will return a
  // promise that contains the value userId.
  return userId;
}

When it comes to the await line, it will wait for the asynchronous createProfileForUser() function to finish (that is, wait for the promise it returns to resolve) before going to the next line, acting like synchronous code that happens in the order written, which can be more intuitive for developers to read and understand.

Errors are caught by wrapping the await call in a try/catch block.

Multiple Calls

It's pretty easy to run multiple promises sequentially with await: Here, everything stops as it waits for createProfileForUser() to finish, and only then does createPictureForUser() begin. When that finishes, this function returns userId.

async function(userId) {
  try {
    await createProfileForUser(userId);
    await createPictureForUser(userId);
  } catch (error) {
    console.error(error);
  }
  return userId;
}

That's probably not what the developer wants to do here, though. These two functions don't really depend on each other. There's no reason they can't start at the same time.

If you thought that async/await terminology meant never having to write Promise again, sorry. We will need Promise.all() to run these in parallel and wait on them both finishing. This is what it looks like:

async function(userId) {
  const profilePromise = createProfileForUser(userId);
  const picturePromise = createPictureForUser(userId);
  try {
    await Promise.all([profilePromise, picturePromise]);
  } catch (error) {
    console.error(error);
  }
  return userId;
}

Again, the catch here will catch the first error in either of those functions.

Await's Return Value

Another handy thing is that you can assign the result of an await statement to a variable, and that value will be the actual value returned by the promise, so you don't have to awkwardly extract it inside a then.

// Note, await returns a value (“userEmail” is a string, not a promise)
const userEmail = await getUserEmail(userId);

If you want the promise, to put into a Promise.all() or pass along, do not use await.

const userEmailPromise = getUserEmail(userId);

You can return either of these from inside an async function, and that function is smart enough to convert whatever you return, to a promise.

Not Included

Some advanced/miscellaneous topics worth looking into include:

  • Promise.race() - Pass it a bunch of promises. It will resolve when the first of those promises finishes. This can be used for a pretty neat cheat to "cancel" promises.
  • Promise cancellation - You can't really. There are many workarounds for this, such as Promise.race().
  • Promise.resolve() - Sometimes some function or another demands a promise and you just need to put in an empty placeholder. This is a fast way to generate a throwaway promise that just resolves immediately. It's kind of the promise equivalent of '' (empty string).
  • What happens when you return from a catch()?
  • Using in serverless functions - forgetting to return something from a promise can cause issues in environments that need to know when execution has finished.
  • Look into how the Javascript event loop works, and why even promises that resolve "immediately" do not execute before the following synchronous line of code.