Understanding Javascript promises

Last updated

Antonio Ufano avatar

Antonio Ufano

I've spend the last weeks coding a lot in JavaScript and, although I've been using promises in my code I wasn't fully sure how they worked. I've been using functions that returned them (like axios get() and post() methods for API calls) but not implementing them myself so, I decided that the best way to understand how they work was to code a a few demos.

Find below a few things I've learnt building them. All code can be found in the following repo in GitLab.

Creating a promise in Javascript

A JS function that returns a promise is similar to someone telling (promising) you he'll do something. If he does it, the promise is fulfilled (it's resolved) and if he doesn't, it's not fulfilled (rejected). For our first example, let's imagine that that "do something" is just a boolean variable:

let conditionToResolve = true

Now we can create a new promise that depends on that variable to resolve

//the promise

let myProm = new Promise((resolve, reject) => {
  //ES6 syntax arrow func
  if (conditionToResolve) {
    const message = 'condition was true so promise is resolved'
    //the resolve method return the object we pass it
    resolve(message)
  } else {
    const err = new Error('condition was false so promise is rejected')
    reject(err)
  }
})

The Promise constructor receives a callback function to be executed with the arguments resolve and reject. When the result of the function is successful, the function returns with resolve (it resolves the promise) and if it's not, returns with reject (it rejects the promise). Now we need to create a function that uses that promise and does something depending if the promise has been resolved or rejected:

function(){
    myProm.then( (resultOfPromise) => {  //ES6 syntax arrow func
    //promise is resolved
        console.log(resultOfPromise);
    }).catch((error) => {
    //promise was rejected
        console.log(error.message);
    })
}

With myProm.then() we obtain whatever is returned when the promise is resolved (in this example, a message). In the catch, we obtain whatever we returned when the promise is rejected (in our case, an Error object).

Important! Promises can be in three different statuses: pending, resolved or rejected. Imagine that we do a loop using the previous promise and function like this:

for (let i = 0; i < 100; i++) {
  askProm()
  //change the condition to check if the next execution returns a different result
  conditionToResolve = !conditionToResolve
}

You can think this will execute our function askProm() 100 times and alternate the result (because we change the condition in each iteration) but it actually returns the same 100 times. This is because once the promise is resolved/rejected the first time, the next iterations are not creating new promise instance (we're not calling the constructor new Promise()), they are evaluating again the original one.

Functions that return a Javascript promise

Now let's build a more complex example. Imagine a function that receives two numbers and, if the sum of them is more than 5, we'll resolve a Promise and if it's less, we'll reject it. It will be like this:

function calcNumbers(a, b) {
  const sum = a + b
  if (sum > 5) {
    return Promise.resolve(sum)
  } else {
    //const err = new Error('REJECTED! Sum is less than 5, it is: ' + sum);
    return Promise.reject(sum)
  }
}

Now we can call this function with different arguments:

calcNumbers(3, 3)
  .then((res) => {
    console.log('RESOLVED! The res was more than 5, it was: ' + res)
  })
  .catch((err) => {
    console.log('REJECTED! Sum is less than 5, it is: ' + err)
  })
//RESOLVED! The res was more than 5, it was: 6
calcNumbers(1, 2)
  .then((res) => {
    console.log('RESOLVED! The res was more than 5, it was: ' + res)
  })
  .catch((err) => {
    console.log('REJECTED! Sum is less than 5, it is: ' + err)
  })
//REJECTED! Sum is less than 5, it is: 3

One difference with the first example is that in this case we're not using the new constructor, but the resolve/reject methods instead. This way, every time we call the function calcNumbers, we're resolving/rejecting a new Promise.

Important! Promises are asynchronous. JS will not wait until the promise is resolved to continue executing the next line of code. If you need to execute something only after the promise has been resolved, that code would go inside the .then() block.Using the example above, let's include some logs before and after each call:

console.log('before first calcNumbers() call')
calcNumbers(3, 3)
  .then((res) => {
    console.log('RESOLVED! The res was more than 5, it was: ' + res)
  })
  .catch((err) => {
    console.log('REJECTED! Sum is less than 5, it is: ' + err)
  })
console.log('between calcNumbers() calls')

calcNumbers(1, 2)
  .then((res) => {
    console.log('RESOLVED! The res was more than 5, it was: ' + res)
  })
  .catch((err) => {
    console.log('REJECTED! Sum is less than 5, it is: ' + err)
  })
console.log('after both calcNumbers() calls')

The output of this will be:

before first calcNumbers() call
between calcNumbers() calls
after both calcNumbers() calls
RESOLVED! The res was more than 5, it was: 6
REJECTED! Sum is less than 5, it is: 3

Chaining promises in Javascript

Promises are used for asynchonous tasks that take some time to complete, like reading a file, retrieving data from a database or from a third party API. The time it takes for these actions to be completed may vary depending on different conditions (larger files takes more time to read, if the network is slow, the API call will take more time) or they may even fail (file doesn't exists or there is no network).

In these cases, promises are very useful; they'll resolve when the action is completed or reject if there is any issue. We can chain them so one function will wait until the previous one is resolved/rejected. See the functions below:

//simulates reading a file, takes 2secs.
function readFile(filename) {
  //if filename is empty rejects promise
  if (filename != '') {
    return new Promise(function (resolve) {
      setTimeout(() => {
        resolve('file OK')
      }, 2000)
    })
  } else {
    const error = new Error('File does not exist')
    return Promise.reject(error)
  }
}

//simulates an API call, takes 3secs
function callApi(fileContent) {
  //if fileContent is diferent from 'file OK' rejects promise
  if (fileContent == 'file OK') {
    return new Promise(function (resolve) {
      setTimeout(() => {
        resolve('Response from API')
      }, 3000)
    })
  } else {
    const reason = 'File is not OK'
    return Promise.reject(reason)
  }
}

//prints the message
function sendResponse(apiRes) {
  console.log('Sending response: ' + apiRes)
  return
}

We have three different functions, to simulate reading a file (readFile), calling an API (callApi) and sending a response (sendResponse). Imagine we need to execute the following actions:

  • Read a file and return its content.
  • Call an API with the content of the file.
  • Send the response of the API.

We can chain the promises by including the next call inside the .then() block of each function like this:

readFile('randmFile.txt')
  .then((responseReadFile) => {
    //if readFile() promise is resolved, pass the response to callApi()
    console.log('readFile() resolved OK. It returned: ' + responseReadFile)
    return callApi(responseReadFile)
  })
  .then((responseCallApi) => {
    //if callApi() promise is resolved, pass the respons to sendResponse()
    console.log('callApi() resolved OK. It returned: ' + responseCallApi)
    return sendResponse(responseCallApi)
  })
  .catch((error) => {
    console.log(error.message)
  })

As you can see, once we capture the return of the promise in the .then() block, we have to return the call to the next function. This way the call to the next function will not be executed until the previous one has been resolved or rejected.

In this example, you can change the value passed to the function readFile() or the value returned when its promise is resolved to see how it fails:

//this will reject with error message 'File does not exist'
readFile('').then( responseReadFile => {

//....
//this will reject with error message 'File is not OK'
    return new Promise(function(resolve) {
            setTimeout( () => {
                resolve('file not OK')
            }, 2000)
    })

Using Javascript ES7 async/await syntax

In ES7 there is a new syntax to define asynchronous functions and to call them which makes our code simpler and easier to read. We just have to prefix our functions with async to indicate they are asynchronous and use await when calling them. In addition, we have to surround the calls to them inside a try/catch block.The functions of the examples above will look like this in ES7:

//async function that simulates reading a file, takes 2secs.
async function readFile(filename) {
  //if filename is empty rejects promise
  if (filename != '') {
    return new Promise(function (resolve) {
      setTimeout(() => {
        resolve('file OK')
      }, 2000)
    })
  } else {
    const error = new Error('File does not exist')
    return Promise.reject(error)
  }
}

//async function that simulates an API call, takes 3secs
async function callApi(fileContent) {
  //if fileContent is diferent from 'file OK' rejects promise
  if (fileContent == 'file OK') {
    return new Promise(function (resolve) {
      setTimeout(() => {
        resolve('Response from API')
      }, 3000)
    })
  } else {
    const reason = 'File is not OK'
    return Promise.reject(reason)
  }
}

//async function that prints the message
async function sendResponse(apiRes) {
  console.log('Sending response: ' + apiRes)
  return
}

Then in order to call the first one, we have to create an anonymous function with a try/catch block like this:

//anonymous async function to start the execution
;(async () => {
  try {
    console.log('before readFile()')
    let responseReadFile = await readFile('awdwad')
    console.log('after readFile(): ' + responseReadFile)

    console.log('before callApi()')
    let responseCallApi = await callApi(responseReadFile)
    console.log('after callApi(): ' + responseCallApi)

    sendResponse(responseCallApi)
  } catch (error) {
    console.log(error)
  }
})()

Conclusion

Most important things to remember are:

  • A Promise can be in pending, resolve or rejected status.
  • A Promise cannot be reused, once it's resolved/rejected, the status cannot change.
  • Promises are used for asynchronous tasks.
  • We can chain functions that return a Promise including the next execution inside the .then() block of the previous one. * Asyn/Await, included in ES7, simplifies readability when chainging multiple functions. Hope this article help you understand JavaScript promises and how to use them. Happy coding!

References:

If you enjoyed this article consider sharing it on social media or buying me a coffee ✌️

Oh! and don't forget to follow me on Twitter where I share tons of dev tips 🤙

Other articles that might help you