How Do Promises Work?

Christopher T.

May 23rd, 2019

Share This Post

How Do Promises Work?

Well, Promises are actually very similar to real life situations. You can literally think of any promise situation in real life and convert it straight into code.

So what is one example, then?

Imagine you have a best friend named Kelly and you wanted to invite her to check out this new coffee shop that opened up in the area.

You decided to text her and now you are waiting for a response back. She will either accept your invitation, or she will reject it. If she accepts, you and Kelly can work together to decide on a date where you are both free to hang out. Otherwise, if she rejects, you won't be going to the coffee shop together. (Unless you decide to invoke the invitation again and maybe she will accept the next time!)

This was an example representing the 3 states of a JavaScript promise:

  1. Pending - You are waiting until she either accepts (resolves) or rejects
  2. Fulfilled - She agreed to go check out the new coffee shop with you
  3. Rejected - She will say no to your invitation

How Do I Create a Promise?

Now to understand it in a more JavaScript-y way, lets convert this analogy to JavaScript code:

Here we have a made up software development kit providing API methods to interact with Kelly.

import kellyApi from 'kelly-sdk'

const makeKelly = ({ mood }) => {
  const _kelly = {}
  if (mood) _kelly.mood = mood

  return {
    ask(question) {
      return new Promise((resolve, reject) => {
        if (question) {
          const answer = kellyApi.askQuestion(question)
          resolve(answer)
        } else reject('No question asked')
      })
    },
  }
}

const kelly = makeKelly({ mood: 'happy' })

Inside the function we return an object that includes the method ask, which is a Promise. It returns a new Promise instance and the arguments it provides is resolve, and reject.

If the function is successful it should call resolve. If not, it should call reject. It accepts arguments which will be passed to the caller as arguments.

How Do I Use Promises?

Now that we've created a promise and can now be used, lets try it out:

const askKelly = (question) => {
  return new Promise((resolve, reject) => {
    kelly
    .ask(question)
    .then((answer) => {
      // Kelly has answered back positively! She accepted!
      // Do stuff
      resolve(answer)
    })
    .catch((error) => {
      // Kelly has rejected
      reject(error)
    })
  })
}

askKelly(
  'Hi kelly, there is a new coffee shop that opened. Would you like to go check it out with me?',
)
.then((result) => console.log(result))
.catch((error) => console.error(error))

Promise Chaining

A neat thing about Promises is that they can be chained. Suppose Kelly has accepted the invitation and now you proceed to schedule a date where you are both free.

We are going to use the moment library to query for a date in the future.

Instead of having to write a separate Promise/function to continue the execution, you can instead chain the Promise by returning the previous Promise:

import moment from 'moment'

const makePlan = (options) => {
  return new Promise((resolve) => {
    resolve({
      date: '',
      ...options,
    })
  })
}
const chooseDate = (isoString) => {
  return new Promise((resolve) => {
    resolve(moment(isoString).format('MMMM Do, YYYY'))
  })
}
let plan
const askKelly = (question) => {
  return new Promise((resolve, reject) => {
    kelly
    .ask(question)
     .then((answer) => {
      // Kelly has answered back positively! She accepted!
      return makePlan({ messages: [] })
    })
        .then((newPlan) => {
      plan = newPlan
      plan.messages.push({ me: question })
      plan.messages.push({ kelly: answer })
      // Choosing a date 14 days in the future
      const futureDateAsISO = moment()
        .add(14, 'days')
        .toISOString()
      return chooseDate(futureDateAsISO)
    })
    .then((readableDate) => {
      console.log(readableDate)
      // output: June 9th, 2019
      // Proceed to check with Kelly if she is free on June 9th
      return kelly.ask(`Great! How does ${readableDate} sound?`)
    })
    .then((answer) => {
      // Kelly's answer
      console.log(answer)
      resolve(answer)
    })
    .catch((error) => {
      // Kelly has rejected
      reject(error)
    })
  })
}

askKelly(
  'Hi kelly, there is a new coffee shop that opened. Would you like to go check it out with me?',
)
.then((result) => console.log(result))
.catch((error) => console.error(error))

Doesn't that look neat? The benefit of promise chaining is that not only does your code look synchronous, but it's also easier to read and easier to debug!

Given the asynchronous nature of promises and how difficult it can get, this is a powerful feature implemented in the JavaScript language and easily avoids the problem of callback hell.

ES7 - Async Await

A powerful feature introduced in ES7 is async/await. These functions allow you to write asynchronous code while making it look synchronous. Normally, people wrap these function blocks with a try/catch in order to properly catch errors.

Here is a rewritten example of our previous example code, using async/await:

import moment from 'moment'

const makePlan = (options) => {
  return new Promise((resolve) => {
    resolve({
      date: '',
      ...options,
    })
  })
}
const chooseDate = (isoString) => {
  return new Promise((resolve) => {
    resolve(moment(isoString).format('MMMM Do, YYYY'))
  })
}
const askKelly = async (question) => {
  try {
    const answer = await kelly.ask(question)
    const plan = await makePlan({ messages: [] })
    plan.messages.push({ me: question })
    plan.messages.push({ kelly: answer })
    // Choosing a date 14 days in the future
    const futureDateAsISO = moment()
      .add(14, 'days')
      .toISOString()
    const readableDate = await chooseDate(futureDateAsISO)
    console.log(readableDate)
    // output: June 9th, 2019
    // Proceed to check with Kelly if she is free on June 9th
    const nextAnswer = await kelly.ask(`Great! How does ${readableDate} sound?`)
    console.log(nextAnswer)
  } catch (error) {
    // Kelly has rejected
  }
}

askKelly(
  'Hi kelly, there is a new coffee shop that opened. Would you like to go check it out with me?',
)
.then((result) => console.log(result))
.catch((error) => console.error(error))

Much shorter and neater, isn't it?

Why Do We Need Promises and When Do We Use Them?

So why do we even need promises? What problems do they solve? Before we dive into this, lets take a step back into the past and see why this was needed:

Normal Functions vs Async Functions

Imagine you need to perform an HTTP request to an API to receive data. This is an asynchronous call, so we need to wait some time for the data to return.

Without promises, we would have to use a callback pattern to wait for the data:

const retrieveDataFromAPI = function(url, callback) {
  const httpRequest = new XMLHttpRequest()
  httpRequest.onreadystatechange = function() {
    if (httpRequest.readyState == 4 && httpRequest.status == 200) {
      // Data received. We can now pass the data into the callback
      if (typeof callback === 'function') {
        callback(httpRequest.response)
      }
    }
  }
  httpRequest.open('GET', url, true)
  httpRequest.responseType = 'json'
  httpRequest.send()
}

retrieveDataFromAPI('https://someapi.com/v1/someendpoint', function(
  responseData,
) {
  console.log(responseData)
})

There is nothing wrong with this type of code style, and the callback pattern is still just as effective as promises today in any application.

However, as a developer, I like to make my life easier and save time by using easier syntax which makes code more readable without having to put in unnecessary time into writing a perfect code architecture (and without defecting code performance).

This is referring to the callback hell problem in callback nature. When callback hell happens, your code starts to look like crap. Bugs start to occur because people are pulling their own hair out trying to read your code.

Suppose we want to call the same api with different parameters based on the response result of the previous call.

Here is what that may look like:

const data = {
    doctors: [],
}

retrieveDataFromAPI('https://someapi.com/v1/someendpoint', function(
  responseData,
) {
  console.log(responseData)
  if (responseData.result) {
      if responseData.result.doctors.length > 0) {
          const doctorIds = responseData.result.doctors.map((doctor) => doctor.id)
          if (doctorIds.length) {
              data.doctors.push(...doctorIds)
          }
          // We have some doctors in the data, now lets grab additional data to populate our data object
          retrieveDataFromAPI(`https://someapi.com/v1/someotherendpoint?ids=${doctorIds}&limit=300`, function(moreData) {
              if (moreData.result) {
                  if (moreData.result.data) {
                      console.log(moreData.result.data)
                      retrieveDataFromAPI(`https://someapi.com/v1/thirdendpoint?limit=300`, function(additionalData) {
                          console.log(additionalData)
                      })
                  }
              }
          })
      }
  }
})

Avoiding Callback Hell--Stepping Into Promise Heaven

With promises, it becomes less cumbersome and the code becomes much easier to read:

const retrieveDataFromAPI = function(url) {
  return new Promise((resolve, reject) => {
    const httpRequest = new XMLHttpRequest()
    httpRequest.onreadystatechange = function() {
      if (httpRequest.readyState == 4 && httpRequest.status == 200) {
        // Data received. We can now pass the data into the callback
        if (typeof callback === 'function') {
          resolve(httpRequest.response)
        }
      } else if (httpRequest.status > 400) {
        reject(httpRequest.statusText)
      }
    }
    httpRequest.open('GET', url, true)
    httpRequest.responseType = 'json'
    httpRequest.send()
  })
}

retrieveDataFromAPI('https://someapi.com/v1/someendpoint')
  .then((responseData) => {
    if (responseData.result) {
      if (responseData.result.doctors.length > 0) {
        const doctorIds = responseData.result.doctors.map((doctor) => doctor.id)
        if (doctorIds.length) {
          data.doctors.push(...doctorIds)
        }
        // We have some doctors in the data, now lets grab additional data to populate our data object
        return retrieveDataFromAPI(
          `https://someapi.com/v1/someotherendpoint?ids=${doctorIds}&limit=300`,
        )
      }
    }
  })
  .then((moreData) => {
    if (moreData.result) {
      if (moreData.result.data) {
        console.log(moreData.result.data)
        return retrieveDataFromAPI(
          `https://someapi.com/v1/thirdendpoint?limit=300`,
        )
      }
    }
  })
  .then((additionalData) => {
    console.log(additionalData)
  })

With promises, we flatten the callback with .then. In a way, it looks cleaner because the nesting is reduced. Of course, with ES7 async syntax, we can even further optimize our example with a more readable approach. Try it!

Summary

Promises are like androids compared to flip phones. It makes your life easier and the code has been widely adopted by JavaScript developers in their daily work flow. If you are writing callbacks and have not written promises at all, I suggest you to get familiar with Promises, and see if it works out for you :)

Happy Coding!


Tags

promise
asynchronous
await
async
try catch
callbacks

© jsmanifest 2019