Alex Karydis
HomePortfolioArticlesAboutContact

All you need to know about javascript promises

Published in Articles
April 10, 2023
5 min read

Table Of Contents

01
Let's conceptualize promises
02
Promise terminology
03
How are promises created?
04
How are promises consumed?
05
Async/await
06
Conclusion
All you need to know about javascript promises

Let’s conceptualize promises

In this article we examine promises, a fundamental concept in the asynchronous world of javascript.

The concept of a promise in javascript involves two parties:

  • The promise giver
  • The promise consumer

For example, let’s imagine me and you having a conversation:

You: I want to understand javascript promises better.

Me: I promise you that once you read this article, you will have a better understanding of javascript promises.

You: Ok Alex, once I finish reading, if I understand better I will comment 😀 otherwise I will comment 😕 .

You asked for something and I promised I could deliver it. You then told me how the outcome of the promise would shape your action.

Keep in mind, the promise giver and consumer have differnt objectives. The promise giver acts upon his/her promise and produces an outcome for that promise. The promise consumer is given an outcome and proceeds accordingly.

In this case the promise giver (me) has to make sure the reader understands promises after reading this article. The promise receiver, after reading the article will receiving an outcome which is either undestanding or not understanding ‘promises’. He/she will then proceed to comment 😀 if promises were understood or comment 😕 if promises are still a mystery.

Promise terminology

A promise begins its lifetime in the pending state.

When a promise reaches a successful outcome, it is moves into the fulfilled state (or the promise is fulfilled).

When a promise fails to get the desired outcome it moves into the rejected state. (or the promise is rejected).

In either case, when a promise is fulfilled or rejected, the promise is said to be settled and the state is irreversible.

When a promise is fulfilled it usually produces an outcome, which is called the value of the promise. It is said a promise is fulfilled with a certain value.

When a promise gets rejected it usually throws an error, which is called the reason of the promise. It is said a promise is rejected for a certain reason.

So with this conceptual model in mind, let’s try to understand promises better.

How are promises created?

Promises are created by invoking the Promise constructor:

new Promise(executor)

The function understandPromises below returns a promise.

function understandPromises(article){
    return new Promise((fulFill, reject) => {
        const understanding = readArticle(article);

        if (understanding){
            fulFill(😀);
        } else {
            reject(😕);
        }
    });
}

The constructor receives a parameter, namely the executor function which will run synchronously immediatetly after the new Promise() expression is run.

The executor settles the promise either by fulfilling it or rejecting it.

This is possible because the executor receives two functions as parameters one that fulfills and one that rejects the promise. Each of this functions receives the value and reason as parameters respectively.

Inside the body of the executor the logic of the asynchronous operation is defined. This logic determines when the fulfill and reject functions will be called.

If you want to know exactly what happens once the new Promise() runs, I recommend you read the Description of the MDN page ‘Promise() constructor’.

How are promises consumed?

.then()

So once a promise created, it will eventually be settled, and depending whether the promise was fulfilled or rejected, the consumer will act accordingly.

Binding a set of actions to the settlement is done by attaching the instance method .then() to the given promise.

const onFulfill = (value) => {
  console.log(value);
};

const onReject = (reason) => {
  throw new Error(reason);
};

understandPromises(article).then(onFulfill, onReject);

In the above operation, understandPromises returns a promise. By calling .then() on the returned promise, we can handle the fulfillement and rejection scenarios of the promise.

When the promise is “fulfilled”, onFulfill is called with the parameter “value” which is the value returned by the successful operation. When the promise is “rejected” onReject is called and has access to the “reason” of the error.

Promise chaining

The method .then() returns a Promise itself, so this allows for Promises to be chained together by the .then() method .

Also, we have access to the .catch() promise method that allows to handle all the errors in one place at the end without the need place an onError methods at every .then() .

A .catch() is literally just this:

somePromise.then(undefined, onError);

The following example illustrates promise chaining:

// get paris coordinates
fetchCoordinates("https://example-api.com/geocodes?city=Paris")
//then using the coordinates result, get the temperature
    .then(coords => fetchTemperature(`https://example-api.com/temperature?lat=${coords.lat}&lon=${coords.lon}`)
// then process the temperature result
    .then((temp) => {
        console.log(`Temperature is ${temp}°C.`)
    })
// and if somethig goes wrong in any of the promises, throw an error
    .catch((e) => {throw new Error(e);});

In the example above, we want to obtain the current temperature in the center of Paris and to achieve this we use two APIs: one for obtaining the coordinates of the city center and one for obtaining the temperature at specific coordinates.

So we call the Geocode API first and then use the result as an input for the Temperature API. Any errors thrown by either of the two promise handlers will be handled at the end with the catch method.

Parallel promises

There are four static promise methods (called aggregators) that assist when dealing with multiple promises that can be treated in parallel.

They all receive an array of promises as a parameter and return a single promise. The state (fullfiled or rejected) and outcome (value or reason) of the returned promise depends on the type of static method used.

Promise.all()

Returned promise gets fulfilled only if all promises are fulfilled. The value of the returned promise is an array with values mapping to each promise.

Returned promise gets rejected if any promise gets rejected. The reason of the returned promise is the reason of the rejected promise.

It is typically used when there are multiple asynchronous tasks that the overall code relies on to work successfully — all of whom we want to fulfill before the code execution continues.

function fetchTemperature(coords) {
  const tempUrl = `https://example-api.com/temperature?lat=${coords.lat}&lon=${coords.lon}`;
  return fetch(tempUrl);
}

function fetchTime(coords) {
  const timeUrl = `https://example-api.com/time?lat=${coords.lat}&lon=${coords.lon}`;
  return fetch(tempUrl);
}

function fetchAltitude(coords) {
  const timeUrl = `https://example-api.com/time?lat=${coords.lat}&lon=${coords.lon}`;
  return fetch(tempUrl);
}

// Promise.all is used to make all calls in parallel
// the resulting promise gets fulfilled if all promises fulfill
Promise.all(fetchTemperature(coords), fetchTime(coords), fetchAltitude(coords))
  .then((results) => {
    console.log(`${results[0].temp}°C in Paris.`);
    console.log(`Time in Paris is ${results[1].time}.`);
    console.log(`Paris is at an altitude of ${results[2].altitude} meters.`);
  })
  .catch((e) => {
    throw e;
  });

Promise.allSettled()

Returned promise gets fulfilled when all promises are settled. The value of the returned promise is an array of objects that describe how each promise settled. Each object has the properties:

  • status: fulfilled | rejected
  • value OR reason

The returned promise cannot be rejected.

Use allSettled() if you need the final result of every promise in the input iterable.

Promise.any()

Returned promise gets fulfilled if any of the promises gets fulfilled. The value of the returned promise is the value of the first promise that gets fulfilled chronologically.

Returned promise gets rejected if all promises gets rejected. The reason of the returned promise is an AggregateError .

Promise.race()

Returned promise gets fulfilled if the first promise to settle, was fulfilled. The value of the returned promise is the value of that fulfilled promise.

Returned promise gets rejected if the first promise to settle, was fulfilled. The reason of the returned promise is the reason of that rejected promise.

Async/await

From Mdn:

The async function declaration declares an async function where the await keyword is permitted within the function body. The async and await keywords enable asynchronous, promise-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains.

Async is a special keyword in javascript that is used at function declarations and allows the use of the await keyword within the function.

Await is a special keyword in javscript used with expressions and when used with promises, it “waits” for the promise’s fulfilled value before continuing execution of code. It enables the use of try and catch syntax instead of using promise chains.

async function getTemperature() {
  try {
    const dataCoords = await fetch(
      "https://example-api.com/geocodes?city=Paris"
    );
    const coords = await dataCoords.json();

    const dataTemp = await fetch(
      `https://example-api.com/temperature?lat=${coords.lat}&lon=${coords.lon}`
    );
    const temp = await dataTemp.json();
    console.log(`Temperature is ${temp}°C.`);
  } catch (e) {
    throw e;
  }
}

Please note that the use of try/catch is essential in order to achieve proper error handling since the abscence of .then()and .catch() leaves no other way to handle errors (unless you want to return Promise.reject() from the async function).

Async/await enables aynchronous operations to be written in a synchronous style but keep in mind that if you want to excute multiple operations that don’t depend on each other, then Promise.all() will be more efficient since it enables parallel execution.

Async functions always return a promise.

async function get1() {
  //will return a fulfilled promise with value of 1
  return 1;
}

async function iAmVoid() {
  // will return a fulfilled promise with value of undefined
  const x = 5;
}

async function wait1() {
  //will return a promise that is fullfiled with value 1.

  return;
  await 1;
}

Therefore, if you call an async function that returns promise value, that value will be wrapped in a promise which is probably not what you want. To be able to use the result, you should call that async function with await.

function getUsers(){
    const users = await db.getUsers();
    return users;
}

//by using await we are able to get the promise value
const users = await getUsers();

users.forEach((u) => {
    console.log(user);
})

The reason why we need to use await in order to get the promise value is that the async function immediately returns a promise which eventually will be fulfilled by the await statement.

Conclusion

  • Promises involved two parties, the one that gives and the one that consumes
  • A promise starts in the pending state. Once it settles it can be either fulfilled with a value or rejected with a reason.
  • A promise is created new Promise(executor). Executor receives the fulfill and reject functions as parameters and its body executes the async operation that will eventually be settled.
  • A promise’s fulfillment value and rejection reason become available through the .then() method.
  • Promise chaining allows that multiple promises are chained together leading to a synchronous execution of multiple promises that depend on each other.
  • Multiple errors can be handled by attaching a .catch() method at the end of the chain.
  • Promises that can be executed in parallel are best served by promise aggregators which are: Promise.all(), Promise.allSettled(), Promise.any() and Promise.race() .
  • A better alternative to promise chains is the use of async/await which waits for the promise result before executing the rest of the code synchronously.
  • In order to handle erros using async/await the use of try/catch blocks is recommended.
  • If an async promise returns a promise value, call it with await.

Tags

#javascript#promises#async/await
Previous Article
Building a Weather Card with HTML, CSS, and JavaScript

Categories

Articles
Portfolio

Related Posts

Building a Weather Card with HTML, CSS, and JavaScript
March 14, 2023
2 min
© 2023, All Rights Reserved.

Quick Links

AboutContact

Social Media