This course has already ended.

Asynchronous JavaScript

JavaScript is single-threaded though asynchronous and non-blocking. How is that achieved?

When a task is being processed in the main thread nothing else can be done at the same time. Let’s take a look at some blocking code:

const btn = document.querySelector('button');
btn.addEventListener('click', () => {
  // Function execution stops here until user
  // interacts with the alert
  alert('You clicked me!');

  // Code proceeds here once user has interacted with the alert
  let pElem = document.createElement('p');
  pElem.textContent = 'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
});

What will happen, when a button is clicked ?

  1. An alert is created. Nothing can be done until a user clicks the alert.

  2. A new paragraph is added to the page

Blocking code live demo.

https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Introducing

When you run a task that will take some time to complete, you should use worker threads instead of main thread. This way a user can still interact with a page, while the task is being processed.

Callbacks

What exactly are these so called callbacks?

Callback is a way to delay function execution. Let’s take a look at following code example:

function add(x, y) {
  return x + y;
};

function addFive(x, referenceToAdd) {
  return referenceToAdd(5, x);
};

addFive(5, add); // 10

Function add takes in two arguments x and y and then returns their sum.

Function addFive takes in two arguments x and a reference to another function. It then calls the function that was passed in with values x and 5.


Function addFive is higher-order function because it takes a function reference as a parameter.

Function add is a callback when it is passed to addFive as an argument.

function add(x, y) {
  return x + y;
}

function higherOrderFunction(x, callback) {
  return callback(5, x);
}

higherOrderFunction(5, add);

What are the cons of using callbacks?

In real-life scenarios you will often make lots of API calls to fetch data and images from databases. Sometimes you will have to for example:

  1. Fetch user data

  2. Fetch something else based on the user data

  3. Fetch something else based on the results of step 2.

This doesn’t sound too complicated but it will start to spiral out of control rather quickly. Let’s take a look at an example with just steps 1 and 2. First we will retrieve the user data from API and once we have it we will retrieve user blog posts. (We use a free testing API which does not contain real user data but some fake data for testing purposes.)

const baseUrl = "https://jsonplaceholder.typicode.com";
const postsUrl = `${baseUrl}/posts`;
const usersUrl = `${baseUrl}/users`;
const userId = 1;

document.getElementById("fetchBtn").addEventListener("click", () => {
  const xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (xhr.readyState == 4) {
      if (xhr.status == 200) {
        document.getElementById("userData").innerText = xhr.responseText;
        const xhr2 = new XMLHttpRequest();
        xhr2.onreadystatechange = function() {
          if (xhr2.readyState == 4) {
            if (xhr2.status == 200) {
              document.getElementById("postsData").innerText = xhr2.responseText;
            } else {
              console.error("Unsuccessful transfer (error code: " + xhr.statusText + ")");
            }
          }
        }

        xhr2.open("GET", `${postsUrl}?userId=${userId}`, true)
        xhr2.send();
      } else {
        console.error("Unsuccessful transfer (error code: " + xhr2.statusText + ")");
      }
    }
  }

  xhr.open("GET", `${usersUrl}/${userId}`, true)
  xhr.send();
})

Now imagine if you had to add a third or fourth retrieval. You’ll quickly end up with the Pyramid of DOOM or what is sometimes also referred to as Callback Hell.

Callback live demo.

Promises

Promises were developed to make it easier to write asynchronous code. Promise object represents the eventual value or failure of asynchronous operation.

Promise is a value that is not necessarily known when it is created. Promise has three different states:

  • Pending

    • Promise has not yet been resolved

  • Fulfilled

    • Promise was successful

    • The value is usable

  • Rejected

    • The operation failed

Promise constructor takes in one argument, a callback function with two parameters, resolve and reject which are both callbacks themselves.

const myFirstPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Success!");
  }, 1000);
});

myFirstPromise.then((successMessage) => {
  console.log("Yay! " + successMessage);
});

We can do async operations inside the Promise. When we have done everything we need to do we can call resolve. If something goes wrong we will need to call reject.

Promise.prototype.then and Promise.prototype.catch return promises so they can be chained.

In our example above we create a new Promise that will resolve after waiting for 1000ms. When executed we first receive a promise in pending state. After one second the promise resolves and we move on the the console.log.


Next let’s take a look at the previous example of retrieving data from an API.

const baseUrl = "https://jsonplaceholder.typicode.com";
const postsUrl = `${baseUrl}/posts`;
const usersUrl = `${baseUrl}/users`;
const userId = 1;

document.getElementById("fetchBtn").addEventListener("click", function() {
  fetch(`${usersUrl}/${userId}`)
    .then((response) => {
      return response.json();
    })
    .then((user) => {
      document.getElementById("userData").innerText = JSON.stringify(user);
      return user;
    })
    .then((user) => {
      fetch(`${postsUrl}?userId=${user.id}`)
        .then((response) => {
          return response.json();
        })
        .then((posts) => {
          document.getElementById("postsData").innerText = JSON.stringify(posts);
        })
    })
    .catch((error) => {
      console.error(error.message);
    })
})

With promises it’s slightly more human-readable. Quite many libraries provide a possibility to use promises for their async operations. JavaScript Fetch is available from almost all environments making it a good choice for retrieving data from APIs.

So promises are more readable than callbacks but they’re still not perfect. Many non-JavaScript developers struggle with the concept of promises. That is why there is more syntactic sugar developed to hide the complexity of promises.

Promises live demo

Exercise

Attention

This exercise should be done in groups and submitted as a group.

Include all group members and make sure that everyone knows how your solution works. Only one group member needs to submit the exercise on behalf of the whole group.

You are going to build a function myFirstPromise that will take shouldFail as a parameter. This function will return a Promise.

  • If shouldFail is true the Promise will reject with return value of Fail.

  • If shouldFail is false the Promise will resolve with return value of Success.

A+ presents the exercise submission form here.

Async/Await

One can declare a function to be asynchronous by using the async keyword:

async function example() {}

const example = async () => {};

Async functions operate in a different order from regular functions via the event-loop. The return value of an async function is an implicit promise.

Inside async function we can use await keyword. Await pauses the function execution and waits for the promise to be resolved.

Let’s run previous example that resolves after one second with async keyword:

function resolveAfter1Second() {
  return new Promise(resolve => {
    setTimeout(function() {
      resolve("success!")
    }, 1000)
  })
}

const runAsync = async () => {
  const response = await resolveAfter1Second();
  console.log("The operation was " + response);
};

runAsync(); // The operation was success!

We can declare the promises exactly like before and then wait for their execution in async functions.

Next let’s take a look at our API example. Can it be improved by using async/await?

const baseUrl = "https://jsonplaceholder.typicode.com";
const postsUrl = `${baseUrl}/posts`;
const usersUrl = `${baseUrl}/users`;
const userId = 1;

const fetchAsync = async url => {
  const response = await fetch(url);
  const data = await response.json();
  return data;
}

document.getElementById("fetchBtn").addEventListener("click", async () => {
  const userData = await fetchAsync(`${usersUrl}/${userId}`);
  document.getElementById("userData").innerText = JSON.stringify(userData);

  const userPosts = await fetchAsync(`${postsUrl}?userId=${userData.id}`);
  document.getElementById("postsData").innerText = JSON.stringify(userPosts);
});

Wow, that’s looking way cleaner!

First let’s take a look at the fetchAsync function. Remember from our previous examples that the fetch returned a promise and that we can await for a promise to resolve? Now we can fetch data from any URL in three lines with a reusable function.

Because we define the callback function of fetchBtn to be asynchronous we can use await in there.

Async/await makes the code easier to read, faster to write and more intuitive to test.

Async/Await live demo

Promise.all

Our previous examples have been about sequentially retrieving data:

Fetch user 1

Fetch posts for user 1

What if we had to fetch data for users 1, 2 and 3? What if there were a hundred users?

Promise.all is great for retrieving data in parallel. It takes an iterable as an argument and returns a Promise. This Promise is fulfilled when everything in the iterable has been fulfilled.

In other words you typically pass an array of promises for Promise.all. Your Promise.all fulfills when every promise in the array of promises has been fulfilled.

Promise.all rejects if any of the promises in the array reject.

Let’s modify our example a little bit. Instead of fetching a user and then information about the posts of that user, we could fetch two users and display their data. These requests can be done in parallel with Promise.all.

const baseUrl = "https://jsonplaceholder.typicode.com";
const usersUrl = `${baseUrl}/users`;
const usersArray = [1, 2];

const fetchAsync = async url => {
  const response = await fetch(url);
  const data = await response.json();
  return data;
}

document.getElementById("fetchBtn").addEventListener("click", async () => {
  // Notice how the map operation takes in an async function which always
  // returns promises and usersData will therefore be an array of promises
  const usersData = usersArray.map(async user => fetchAsync(`${usersUrl}/${user}`));
  Promise.all(usersData).then(completed => {
    completed.map((user, index) => {
      document.getElementById(`userData${index + 1}`).innerText = JSON.stringify(user);
    });
  });
})

usersData is an array of promises. When we pass usersData as an argument to Promise.all we receive a single promise. When that single promise resolves we move on to the then statement where we have the data available.

Promise.all live demo

Closing words and more resources

This chapter introduced many quite complex concepts in a short amount of time.

The asynchronous nature of JavaScript confuses many new web developers and causes tons of unexpected behaviours which in turn causes the same questions to appear in StackOverflow over and over again. You should practice using asynchronous JavaScript and read more about it.


Here is a great slow-paced YouTube tutorial about asynchronous JavaScript.

Posting submission...