On the web, many things tend to be time-consuming – if you query an API, it can take a while to receive a response. Therefore, asynchronous programming is an essential skill for developers.

When working with asynchronous operations in JavaScript, we often hear the term Promise. But it can be tricky to understand how they work and how to use them.

Unlike many traditional coding tutorials, in this tutorial we'll learn by doing. We'll complete four tasks by the end of the article:

  • Task 1: Promise basics explained using my birthday
  • Task 2: Build a guessing game
  • Task 3: Fetch country info from an API
  • Task 4: Fetch a country's neighboring countries

If you want to follow along, be sure to download the resources here: https://bit.ly/3m4bjWI

Task 1: Promise basics explained using my birthday

Alt Text

My friend Kayo promises to make a cake for my birthday in two weeks.

If everything goes well and Kayo doesn't get sick, we'll have a certain number of cakes. (Cakes are a countable in this tutorial 😆). Otherwise, if Kayo gets sick, we'll have no cakes.

Either way, we're still going to have a party.

For this first task, we'll translate this story into code. First, let's create a function that returns a Promise:

const onMyBirthday = (isKayoSick) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (!isKayoSick) {
        resolve(2);
      } else {
        reject(new Error("I am sad"));
      }
    }, 2000);
  });
};

In JavaScript, we can create a new Promise with new Promise(), which takes in a function as an argument: (resolve, reject) => {}.

In this function, resolve and reject are callback functions that are provided by default in JavaScript.

Let's take a closer look at the code above.

When we run the onMyBirthday function, after 2000ms:

  • If Kayo is not sick, then we run resolve with 2 as the argument
  • If Kayo is sick then we run reject with new Error("I am sad") as the argument. Even though you can pass anything to reject as an argument, it's recommended to pass it an Error object.

Now, because onMyBirthday() returns a Promise, we have access to the then, catch, and finally methods.

And we also have access to the arguments that were passed into resolve and reject earlier within then and catch.

Let's take a closer look at the code.

If Kayo is not sick:

onMyBirthday(false)
  .then((result) => {
    console.log(`I have ${result} cakes`); // In the console: I have 2 cakes  
  })
  .catch((error) => {
    console.log(error); // Does not run
  })
  .finally(() => {
    console.log("Party"); // Shows in the console no matter what: Party
  });

If Kayo is sick:

onMyBirthday(true)
  .then((result) => {
    console.log(`I have ${result} cakes`); // does not run 
  })
  .catch((error) => {
    console.log(error); // in console: Error: I am sad
  })
  .finally(() => {
    console.log("Party"); // Shows in the console no matter what: Party
  });

Alright, so by now, I hope you get the basic idea of Promise. Let's move onto task 2.

Task 2: Build a guessing game

The requirements:

  • User story: A user can enter a number
  • User story: The system picks a random number from 1 to 6
  • User story: If the user's number is equal to a random number, give the user 2 points
  • User story: If the user's number is different than the random number by 1,
    give the user 1 point. Otherwise, give the user 0 points
  • User story: The user can play the game as long as they want to

For the first 4 user stories, let's create an enterNumber function and return a Promise:

const enterNumber = () => {
  return new Promise((resolve, reject) => {
    // Let's start from here
  });
};

The first thing we need to do is to ask for a number from user and pick a random number between 1 and 6:

const enterNumber = () => {
  return new Promise((resolve, reject) => {
    const userNumber = Number(window.prompt("Enter a number (1 - 6):")); // Ask the user to enter a number
    const randomNumber = Math.floor(Math.random() * 6 + 1); // Pick a random number between 1 and 6
  });
};

Now, userNumber can enter a value, that is not a number. If so, let's call the reject function with an error:

const enterNumber = () => {
  return new Promise((resolve, reject) => {
    const userNumber = Number(window.prompt("Enter a number (1 - 6):")); // Ask user to enter a number
    const randomNumber = Math.floor(Math.random() * 6 + 1); // Pick a random number between 1 and 6

    if (isNaN(userNumber)) {
      reject(new Error("Wrong Input Type")); // If the user enters a value that is not a number, run reject with an error
    }
  });
};

The next thing we want to do is to check if the userNumber is equal to randomNumber, if so, we want give user 2 points and we can run the resolve function passing an object { points: 2, randomNumber }. Notice here that we also want to know the randomNumber when the Promise is resolved

If the userNumber is different than randomNumber by one, then we give the user 1 point. Otherwise, we give the user 0 points:

return new Promise((resolve, reject) => {
  const userNumber = Number(window.prompt("Enter a number (1 - 6):")); // Ask the user to enter a number
  const randomNumber = Math.floor(Math.random() * 6 + 1); // Pick a random number between 1 and 6

  if (isNaN(userNumber)) {
    reject(new Error("Wrong Input Type")); // If the user enters a value that is not a number, run reject with an error
  }

  if (userNumber === randomNumber) {
    // If the user's number matches the random number, return 2 points
    resolve({
      points: 2,
      randomNumber,
    });
  } else if (
    userNumber === randomNumber - 1 ||
    userNumber === randomNumber + 1
  ) {
    // If the user's number is different than the random number by 1, return 1 point
    resolve({
      points: 1,
      randomNumber,
    });
  } else {
    // Else return 0 points
    resolve({
      points: 0,
      randomNumber,
    });
  }
});

Alright, let's also create another function to ask if the user wants to continue the game:

const continueGame = () => {
  return new Promise((resolve) => {
    if (window.confirm("Do you want to continue?")) { // Ask if the user want to continue the game with a confirm modal
      resolve(true);
    } else {
      resolve(false);
    }
  });
};

Notice here that we create a Promise, but it does not use the reject callback. This is totally fine.

Now let's create a function to handle the guess:

const handleGuess = () => {
  enterNumber() // This returns a Promise
    .then((result) => {
      alert(`Dice: ${result.randomNumber}: you got ${result.points} points`); // When resolve is run, we get the points and the random number 
      
      // Let's ask the user if they want to continue the game
      continueGame().then((result) => {
        if (result) {
          handleGuess(); // If yes, we run handleGuess again
        } else {
          alert("Game ends"); // If no, we show an alert
        }
      });
    })
    .catch((error) => alert(error));
};

handleGuess(); // Run handleGuess function

Here when we call handleGuess, enterNumber() now returns a Promise:

  • If the Promise is resolved, we call the then method and show an alert message. We also ask if the user wants to continue.
  • If the Promise is rejected, we show an alert message with the error.

As you can see, the code is quite difficult to read.

Let's refactor the handleGuess function a bit using the async/await syntax:

const handleGuess = async () => {
  try {
    const result = await enterNumber(); // Instead of the then method, we can get the result directly by just putting await before the promise

    alert(`Dice: ${result.randomNumber}: you got ${result.points} points`);

    const isContinuing = await continueGame();

    if (isContinuing) {
      handleGuess();
    } else {
      alert("Game ends");
    }
  } catch (error) { // Instead of catch method, we can use the try, catch syntax
    alert(error);
  }
};

You can see that we created an async function by putting async before the brackets. Then in the async function:

  • Instead of the then method, we can get the results directly just by putting await before the promise
  • Instead of the catch method, we can use the try, catch syntax

Here's all the code for this task again for your reference:

const enterNumber = () => {
  return new Promise((resolve, reject) => {
    const userNumber = Number(window.prompt("Enter a number (1 - 6):")); // Ask the user to enter a number
    const randomNumber = Math.floor(Math.random() * 6 + 1); // Pick a random number between 1 and 6

    if (isNaN(userNumber)) {
      reject(new Error("Wrong Input Type")); // If the user enters a value that is not a number, run reject with an error
    }

    if (userNumber === randomNumber) { // If the user's number matches the random number, return 2 points
      resolve({
        points: 2,
        randomNumber,
      });
    } else if (
      userNumber === randomNumber - 1 ||
      userNumber === randomNumber + 1
    ) { // If the user's number is different than the random number by 1, return 1 point
      resolve({
        points: 1,
        randomNumber,
      });
    } else { // Else return 0 points
      resolve({
        points: 0,
        randomNumber,
      });
    }
  });
};

const continueGame = () => {
  return new Promise((resolve) => {
    if (window.confirm("Do you want to continue?")) { // Ask if the user want to continue the game with a confirm modal
      resolve(true);
    } else {
      resolve(false);
    }
  });
};

const handleGuess = async () => {
  try {
    const result = await enterNumber(); // Instead of the then method, we can get the result directly by just putting await before the promise

    alert(`Dice: ${result.randomNumber}: you got ${result.points} points`);

    const isContinuing = await continueGame();

    if (isContinuing) {
      handleGuess();
    } else {
      alert("Game ends");
    }
  } catch (error) { // Instead of catch method, we can use the try, catch syntax
    alert(error);
  }
};

handleGuess(); // Run handleGuess function

Alright, we are done with the second task. Let's move on to the third one.

Task 3: Fetch country info from an API

You'll see Promises used a lot when fetching data from an API.

If you open https://restcountries.eu/rest/v2/alpha/col in a new browser, you will see the country data in JSON format.

By using the Fetch API, we can fetch the data by:

const fetchData = async () => {
  const res = await fetch("https://restcountries.eu/rest/v2/alpha/col"); // fetch() returns a promise, so we need to wait for it

  const country = await res.json(); // res is now only an HTTP response, so we need to call res.json()

  console.log(country); // Columbia's data will be logged to the dev console
};

fetchData();

Now that we have the country data we want, let's move onto the last task.

Task 4: Fetch a country's neighboring countries

If you open task 4, you will see that we have a fetchCountry function, that fetches the data from the endpoint: https://restcountries.eu/rest/v2/alpha/${alpha3Code} where alpha3code is the code of the country.

You also see that it will catch any error that might happen when getting the data.

// Task 4: get the neigher countries of Columbia

const fetchCountry = async (alpha3Code) => {
  try {
    const res = await fetch(
      `https://restcountries.eu/rest/v2/alpha/${alpha3Code}`
    );

    const data = await res.json();

    return data;
  } catch (error) {
    console.log(error);
  }
};

Let's create a fetchCountryAndNeighbors function and fetch Columbia's information by passing col as the alpha3code.

const fetchCountryAndNeighbors = async () => {
  const columbia = await fetchCountry("col");

  console.log(columbia);
};

fetchCountryAndNeighbors();

Now, if you look in your console, you can see an object look like this:

Alt Text

In the object, there is a border property which is a list of alpha3codes for Columbia's neighboring countries.

Now if we try to get the neighboring countries by:

  const neighbors = 
    columbia.borders.map((border) => fetchCountry(border));

Then, neighbors will be an array of Promise objects.

When working with an array of promises, we need to use Promise.all:

const fetchCountryAndNeigbors = async () => {
  const columbia = await fetchCountry("col");

  const neighbors = await Promise.all(
    columbia.borders.map((border) => fetchCountry(border))
  );

  console.log(neighbors);
};

fetchCountryAndNeigbors();

In the console, we should be able to see list of country objects.

Here's all the code for task 4 again for your reference:

const fetchCountry = async (alpha3Code) => {
  try {
    const res = await fetch(
      `https://restcountries.eu/rest/v2/alpha/${alpha3Code}`
    );

    const data = await res.json();

    return data;
  } catch (error) {
    console.log(error);
  }
};

const fetchCountryAndNeigbors = async () => {
  const columbia = await fetchCountry("col");

  const neighbors = await Promise.all(
    columbia.borders.map((border) => fetchCountry(border))
  );

  console.log(neighbors);
};

fetchCountryAndNeigbors();

Conclusion

Alt Text

After completing these 4 tasks, you can see that Promise is useful when it comes to asynchronous actions or things that are not happening at the same time.

You can see this in practice in one of my tutorials, where we build an application from scratch with React and Next.js:

__________ 🐣 About me __________