Everything Everywhere All at Once: JavaScript Promises

Promises as a concept are hard to grasp because you must understand the JavaScript event loop mechanism, callbacks, and asynchronous programming (async await) to be able to understand Promises. In this article, I will explain all these things.

Everything Everywhere All at Once: JavaScript Promises
Photo by alise storsul / Unsplash

A few days ago, a good friend of mine sent this meme to our WhatsApp group:

So I thought, let's write an article about JavaScript promises.

They can be a bit tough to swallow in the beginning, but the more you work with them, the more you understand them.

Promises as a concept are hard to grasp because you must understand:

  • event loop mechanism,
  • callbacks,
  • asynchronous programming (async await)

to be able to understand Promises. In this article, I will explain all these things.

Good old days

I remember the good old days when you would code everything in HTML/CSS. Later it was a bit of jQuery, here and there some API calls and that's it.

Today, data is fetched over the API endpoints, which means you have to handle a lot of HTTP requests in your code. That implies you must know how to work with asynchronous programming.

We all know that JavaScript is a single-threaded programming language and it uses a synchronous execution model. In plain English, it can process statements in the code one by one.

Imagine you have to wait until data is fetched from the API to write text in the input field or click the button on the website. That would happen if HTTP requests were performed synchronously. This behavior is also known as blocking because it blocks users from interacting with the website.

To fix this blocking behavior, web browsers have integrated special APIs. JavaScript can use those to execute code asynchronously. With this, those heavy operations like fetching the data from API, can be run in parallel with other synchronous operations. With this, users are not blocked and can use the website without any problems.

As a JavaScript developer, you should know how to handle these operations. You also have to understand asynchronous behavior, event loop mechanism, callbacks, and promises.

JavaScript event loop

As it's stated before, JavaScript executes code statements one by one. We can confirm that with a code example that prints our favourite Pokemons in alphabetical order:

function chooseBulbasaur() {
  console.log('I choose you Bulbasaur!')
}

function chooseCharmander() {
  console.log('I choose you Charmander!')
}

function chooseSquirtle() {
  console.log('I choose you Squirtle!')
}

chooseBulbasaur()
chooseCharmander()
chooseSquirtle()

// prints:
// I choose you Bulbasaur!
// I choose you Charmander!
// I choose you Squirtle!

If we modify this code to use asynchronous web API to execute some code, like setTimeout, things will change:

function chooseBulbasaur() {
  console.log('I choose you Bulbasaur!')
}

function chooseCharmander() {
  setTimeout(() => {
    console.log('I choose you Charmander!')
  }, 0)
}

function chooseSquirtle() {
  console.log('I choose you Squirtle!')
}

chooseBulbasaur()
chooseCharmander()
chooseSquirtle()

// prints:
// I choose you Bulbasaur!
// I choose you Squirtle!
// I choose you Charmander!

We know that setTimeout is a function which receives 2 arguments:

  1. a function that should be executed after X milliseconds of time. In the example above, it is a function that logs "I choose you Charmander" to the console
  2. a number of milliseconds

You might be wondering, in the above example number of milliseconds is set to 0, so it should be executed immediately. Yet it doesn't work that way.

Photo by Akin Cakiner / Unsplash

setTimeout is a function that is executed asynchronously because it executes some code after X milliseconds.

It would be very bad if it was executed synchronously. Because then users would wait for X milliseconds until they could use your website again. So if you set 0 or 10 milliseconds, it doesn't matter to JavaScript. If the code is executed asynchronously, it means that it is executed right after synchronous top-level functions.

JavaScript knows what statement should be executed next with the help of the event loop mechanism. To be able to understand event loop, you must understand concepts like Stack and Queue. Those are used behind the curtains.

JavaScript Stack and Queue (FIFO and LIFO)

You can imagine a Stack as a normal array that removes elements in a "last in, first out" (LIFO) manner. This means you can only remove elements from the end of the Stack.

Stack is important in JavaScript as it holds the context of what function is currently running or being executed. JavaScript will remove elements from the Stack one by one, and move to the next one until there is nothing left.

Let's write how the JavaScript Stack executes functions from our synchronous example:

  1. Push chooseBulbasaur function to the Stack, execute it, print "I choose you Bulbasaur!" to the console and finally remove chooseBulbasaur from the Stack
  2. Push chooseCharmander function to the Stack, execute it, print "I choose you Charmander!" to the console and finally remove chooseCharmander from the Stack
  3. Push chooseSquirtle function to the Stack, execute it, print "I choose you Squirtle!" to the console and finally remove chooseSquirtle from the Stack

Very simple and nothing complicated. Now let's see how it behaves for the asynchronous code:

  1. Push chooseBulbasaur function to the Stack, execute it, print "I choose you Bulbasaur!" to the console and finally remove chooseBulbasaur from the Stack
  2. Push chooseCharmander function to the Stack. Execute it.
    - Push the setTimeout function to the Stack.
    - Execute setTimeout.
    - setTimeout will start a timer and will add the anonymous function that was inside it to the Queue.
    - Remove setTimeout from the Stack
  3. Push chooseSquirtle function to the Stack, execute it, print "I choose you Squirtle!" to the console and finally remove chooseSquirtle from the Stack
  4. After everything is done, the JavaScript event loop will check for additional context in the Queue.
    - It will find the anonymous function that was previously added by setTimeout.
    - It will add that function to the Stack.
    - The Stack will pick up that function.
    - Execute it.
    - Print "I choose you Charmander!" to the console,
    - Finally remove the anonymous function from the Stack

If you are more of a video person, this video helped me a lot to understand the JavaScript event loop mechanism. If you have difficulties understanding FIFO and LIFO, this image explains it pretty well:

We mentioned Queue before so let's explain it.

JavaScript also uses a Queue (task queue) in the background. You can imagine the Queue as the lounge or waiting room for functions, while Stack is the office of JavaScript. When the Stack is empty, JavaScript will check if there is something in the Queue. If a Stack is empty and something is in the Queue, JavaScript will call it and execute it.

That's exactly what happened in the asynchronous example above. The anonymous function in setTimeout was executed after all other top-level functions.

Important to note that 0 milliseconds doesn't mean that the anonymous function will be executed after 0 milliseconds. It means that after X milliseconds it will be added to the Queue.

If the function was executed in the Stack right after X milliseconds, it could interrupt with currently executed function in the Stack. Then we would get all sorts of problems. With the Queue, we don't have these problems.

JavaScript callbacks

Callbacks were first tried to solve problems related to asynchronous code. The solution with callbacks is very simple.

You pass the callback function as an argument into a higher-order function. Then that function executes the callback argument function at the proper time.

Let's code the callback solution for the previous example:

function chooseBulbasaur() {
  console.log('I choose you Bulbasaur!')
}

function chooseCharmander(callback) {
  setTimeout(() => {
    console.log('I choose you Charmander!')
    
    // execute callback function
    callback()
      
  }, 0)
}

function chooseSquirtle() {
  console.log('I choose you Squirtle!')
}

chooseBulbasaur()
chooseCharmander(chooseSquirtle)

// prints:
// I choose you Bulbasaur!
// I choose you Charmander!
// I choose you Squirtle!

It is important to remember that callbacks are not asynchronous, but setTimeout is. Callbacks are just a mechanism to be notified when asynchronous code is executed. Then you can decide what you should do next after success or error.

Callbacks are fine, but there is another side to them. They can cause damage to the readability of code so much that they have a very unique name for that.

You probably heard about "Callback hell" or "Pyramid of Doom", but let's see how it looks:

function pyramidOfDoom() {
  setTimeout(() => {
    console.log('I choose you Bulbasaur!')
    setTimeout(() => {
      console.log('I choose you Charmander!')
      setTimeout(() => {
        console.log('I choose you Squirtle!')
      }, 300)
    }, 1200)
  }, 600)
}

// prints:
// I choose you Bulbasaur!
// I choose you Charmander!
// I choose you Squirtle!

You see these nestings. Not nice, isn't it?

Well, this is definitely not the worst case but it can get a lot worse. Imagine if all these setTimeouts were separate functions, with response and error arguments. Then you need to handle errors and do stuff with responses.

Due to all these problems, the ES6 JavaScript standard introduced Promises to solve these problems.

JavaScript promises

A Promise is simply an object that might return value sometime in the future. It defines the fulfillment of an asynchronous operation. It achieves the same goal as the callback function but offers more tools and has cleaner syntax.

Promise can have 3 states:

  • fulfilled - Success, promise has been resolved
  • rejected - Fail, promise has been rejected
  • pending - Default state before being resolved or rejected

Let's see code examples for all these states:

const resolvedPromise = new Promise((resolve, reject) => {
  resolve('I am resolved!')
})
// Promise { <state>: "fulfilled", <value>: "I am resolved!" }


const rejectedPromise = new Promise((resolve, reject) => {
  reject('I am rejected!')
})
// Promise { <state>: "rejected", <reason>: "I am rejected!" }


const promise = new Promise((resolve, reject) => {})
// Promise { <state>: "pending", value: undefined }

As you can see you can initialize Promise with a new keyword and you have to pass a function inside it. That function has to have resolve and reject parameters:

  • resolve - handles the success scenario of the Promise.
  • reject - handles the failure scenario of the Promise.

Now that you know how to create a Promise, let's see some examples of how you can consume a Promise.

This will be a more frequent scenario in your day-to-day work than creating a Promise.

Functions you can use to consume a Promise:

  • .then() - handles resolve scenario, will execute onFullfiled function asynchronously
  • .catch() - handles reject scenario, will execute onRejected function asynchronously
  • .finally() - handles scenario when Promise is settled, this is execute onFinally function asynchronously

Let's take a look at this example:

const choosePikachu = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Pikachu'), 2000)
})


choosePikachu.then((response) => {
  console.log(response)
})
// prints "Pikachu"

As you can see, you can easily consume Promise with .then syntax. This will print the "Pikachu" message in the console after 2000 ms.

Photo by Michael Rivera / Unsplash

Remember that you can also chain Promises:

choosePikachu
  .then((firstForm) => {
    // Return a new value for the next .then
    return firstForm + ' evolves to Raichu!'
  })
  .then((secondForm) => {
    console.log(secondForm)
  })
  
// prints "Pikachu evolves to Raichu"

This comes in handy in a lot of situations, for example when you fetch data from an external API:

fetch('https://pokeapi.co/api/v2/pokemon/pikachu')
  .then((resp) => {
    return resp.json()
  })
  .then((data) => {
    console.log(data)
  })
  .catch((err) => {
    console.error(err)
  })

But also, not all Promises will be fullfiled. Some of them will result in errors, so you need to handle these as well with .catch syntax.

Here is an example:

function getPokemons(isSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => { 
      if (isSuccess) {        
        resolve([          
          { id: 1, name: 'Bulbasaur' },          
          { id: 2, name: 'Charmander' },          
          { id: 3, name: 'Squirtle' },        
        ])      
      } else {        
        reject('An error occurred on fetching pokemons!')      
      }    
      }, 3000)  
   })
}

getPokemons(false)
  .then((response) => {
    console.log(response)
  })
  .catch((error) => {
    console.error(error)
  })

// prints "An error occurred on fetching pokemons!"

As you can see, the function getPokemons returns Promise. That Promise will return the pokemons data only if the isSuccess parameter is truthy. Otherwise, it will result in an error.

After the function definition, we execute the getPokemons function.  The promise is consumed with .then and .catch chained functions. The promise is rejected because the argument in the getPokemons function is false. Promise will be rejected. This will result in the message "An error occurred on fetching pokemons".

This will be the most frequent scenario of how you can handle API requests in your app. For example, to fetch the data from the back end and display it in your app.

Async await

In the ES 2017 standard, JavaScript introduced async and await keywords. These keywords come in handy if you don't want to use .then syntax. With async await you have a feeling that the code is synchronous, although it is not.

The syntax is a bit different, but in the background, it still uses good old Promises:

async function fetchPokemon() {
  return { name: 'Pikachu' }
}

console.log(fetchPokemon())

// prints Promise { <state>: "fulfilled", <value>: {name: 'Pikachu'} }

As you can see return value is still Promise. This means you can use .then syntax on async functions, like this:

fetchPokemon().then((resp) => console.log(resp))

// prints { name: 'Pikachu' }

For the await keyword, you can use it in an async function when you want to wait for Promise to be fullfiled before moving to the next line.

Here is an example:

async function fetchPokemon() {
  const resp = await fetch('https://pokeapi.co/api/v2/pokemon/pikachu')
  const pokemon = await resp.json()
  console.log(pokemon)
}
 
fetchPokemon()

// prints 
// { 
//   abilities: (2) […], 
//   base_experience: 112, 
//   forms: (1) […], 
//   game_indices: (20) 
//   name: "pikachu"
//   ...
// }

It is also useful to note, that we can handle errors with classic try-catch block instead of .catch() that we used with .then() syntax:

async function fetchPokemon() {
  try {        
    const resp = await fetch('https://pokeapi.co/api/v2/pokemon/pikachu')
    const pokemon = await resp.json()
    console.log(pokemon)
  } catch (err) {    
    console.error(err)  
  }
}

Conclusion

Most of the modern projects use async-await syntax. But to be able to understand it fully, developers must know all about asynchronous code.

For example, sometimes you will use Promise.all and you won't be able to do so until you understand everything that is mentioned before.

In this article, we discovered how the host environment uses the event loop to manage code execution orders using the Stack and Queue. We explored three methods for handling asynchronous events:

  • callbacks,
  • promises,
  • async/await syntax.

We also learned how to use the fetch Web API for async actions.