DEV Community

SavagePixie
SavagePixie

Posted on

An introduction to promises in JavaScript

Promises allow us to perform asynchronous operations. A Promise is a proxy, a placeholder if you will, for a value that is not necessarily known when we declare the promise. Instead of immediately having the final value, we have a promise that a final value will be there.

They are useful when we need to do things such as store or retrieve data from a database or get data from an API.

How to create a Promise

To create a promise we simply need to make a new instance of the object and pass a function as a parameter with the resolve and reject parameters.

const promise = new Promise((resolve, reject) => /* do things */)

resolve will be called if the asynchronous action completes successfully and reject will be called if it doesn't. A promise can have three different states:

  • pending is its initial state, it means that it hasn't completed yet
  • fulfilled means that the operation has resolved or completed successfully
  • rejected means that the operation has failed

So when the promise is first created, its state will be pending. Then, once the asynchronous operation has taken place, if it resolved successfully its state will become fulfilled and it will call the function resolve. Otherwise, it will be rejected and call the function reject.

So a quick example of a promise could look like this:

const promise = new Promise((resolve, reject) => {
   console.log('Asynchronous operation started')
   setTimeout(() => Math.random() > 0.15
      ? resolve('Success!')
      : reject('Oops, something went wrong!')
   , Math.random() * 700 + 800)
})

The first thing that we'll get here is a message in our console letting us know that the operation has started. Then, after 0.8 to 1.5 seconds, the promise will either resolve (~85% of the time) and return a success message or fail (~15% chance) and return a failure message.

then and catch or what happens when the promise resolves

Most often, after the asynchronous operation has resolved, we'll want to do something with the returned data. If, for example, we are retrieving information from a database, we might want to actually use that information. That's where the methods then and catch come in handy.

then

The method then accepts two optional parameters, onFulfilled and onRejected. The first one will be called if the promise is fulfilled and the second one if it is rejected. Both functions will get one argument, which is the value returned by the promise.

Building on our previous promise, it could look something like this:

promise.then(data => {
   writeMsg(data) // Writes 'Success!'
   launchFireworks() // Launches fireworks
}, rejection => {
   writeMsg(rejection) // Writes 'Oops, something went wrong!'
   playDefeatMusic() // Plays sad, defeat music
})

Often, though, you'll just want to pass the onFulfilled parameter and leave the logic that deals with rejection for a catch method. So you we could just write this:

promise.then(data => {
   writeMsg(data)
   launchFireworks()
})

If you only need to pass one function to the then, you can just pass its name and the then will take care of calling it and passing the result of the promise as the function's argument.

//Both these thens do the same
promise.then(data => doStuff(data))
promise.then(doStuff)

catch

The method catch accepts the parameter onRejected, which will be called if the promise rejects. Other than that, it works exactly as then.

promise
   .then(data => {
      writeMsg(data)
      launchFireworks()
   })
   .catch(error => {
      writeMsg(error)
      playDefeatMusic()
   })

And just like then, you can use shorthand when calling it:

promise
   .then(doStuff)
   .catch(logError)

Chaining then and catch

Whatever is returned by then and catch will also be wrapped in a promise. So it is possible to chain them even if they are not really doing asynchronous stuff.

promise
   .then(transformData)
   .then(doMoreAsyncStuff)
   .then(transformData)
   .catch(dealWithError)

But maybe it's time we looked at a real example, instead of something filled with mock functions. Let's suppose that we are using MongoDB to store data about our exercise sessions. At some point, we want to retrieve said data. So we could do something like this:

const mongoDB = require('mongodb')

mongoDB.MongoClient.connect(URI)
   .then(client => client.db('exercise'))
   .then(db => db.collection('workouts').find(query))
   .then(data => data.toArray())
   .then(console.log)
   .catch(console.warn)

This creates a connection to our MongoClient, which already returns a promise by itself. Then it selects the database exercise. Then it selects the collection workouts and looks for something that matches the criteria specified in query. Then it transforms the returned data into an array. Then, if everything has gone well, it logs the data into our console. If anything goes awry in the process, it will log it as a warning in the console.

Making a function that returns a promise

If we use MongoDB, fetch or any function that returns a promise, we can just chain then and catch methods to it and that's all that we need to do to work with promises. But this isn't always the case. Sometimes, we might need to create a function that returns a promise first.

For example, let's imagine that for our exercise database we decided to use some database whose API for JavaScript doesn't return promises. Instead, it takes callbacks to deal with the returned data. So we would have to do something like DbHandler.find(query, callback) when we want to do something with retrieved information. And let's imagine that the callback should take two parameters data and error, which will be the retrieved data and the errors that might have happened.

Then we can create a function that looks up stuff in the database and returns it as a promise:

const findPromise = query => new Promise((resolve, reject) => {
   DbHandler.find(query, (data, error) => {
      if (error == null) return resolve(data)
      else return reject(error)
   }
})

And now when we want to look up stuff in our database, we can call our crafted function like any other function that returns a promise:

findPromise(query)
   .then(doStuff)
   .catch(console.warn)

Latest comments (4)

Collapse
 
therealgrinny profile image
Connor

This is an awesome tutorial on promises. Clean, quick and useful. 😁👍

Collapse
 
savagepixie profile image
SavagePixie

Thanks a lot for the feedback!

Collapse
 
avalander profile image
Avalander • Edited

Nice article, just a small comment.

As long as there is a return statement (either explicit or implicit) in the callback function, both then and catch will return a promise (otherwise they'll return undefined).

In javascript, a function that doesn't return anything technically returns undefined, you can still chain those functions in a promise chain, it's just that the resolved value will be undefined.

fetchSomething()
  .then(data => {
    console.log(data)
    // This function doesn't return anything
  })
  .then(data => {
    // data is undefined, but it's still wrapped inside a promise.
  })
Enter fullscreen mode Exit fullscreen mode
Collapse
 
savagepixie profile image
SavagePixie • Edited

Good catch! I guess I should reword that.
Thanks^