Asynchronous JavaScript

Understanding promises, async/await, and the event loop.

#intermediate

#async

#promises

#event-loop

You may have heard of the term “asynchronous” or “async” Javascript, but what exactly does it mean and where is it used?

Let’s think of Javascript in the standard sense:

for (let i = 0, i <= 3, i++) {
  console.log(`i is: ${i}`);
}
console.log('end of loop')

When we have Javascript like this, we expect our for loop to run completely then, and only then, will we see our end message.

'i is: 0'
'i is: 1'
'i is: 2'
'i is: 3'
'end of loop'

And that makes sense, Javascript is (in this default state) blocking meaning that each operation must finish completely for the next to execute. The ‘end of loop’ log must wait for the for loop to finish.

However, Javascript also has nonblocking functionality as well where you can specify chunks to execute simultaneously. Why is this useful? Let’s think about API calls.

API Calls and Javascript

Imagine you’re on the gameshow “Who Wants to Be a Millionaire”. In this gameshow, the host asks you various trivia questions. Get them right and you could win a million dollars!

Of course, the questions can be quite difficult and you’re given some “lifelines” if you need help. One of these lifelines is to phone a friend who might know the answer to a question. Let’s say you use this lifeline on a question.

When you phone your friend, you don’t immediately answer the question and then hang up. That would be rude! And that’s what blocking Javascript would do.

However, what you should do is:

  1. Wait for your friend to pick up
  2. Tell them the question
  3. Wait for their answer
  4. Hang up the call
  5. Answer the question

With these, you have technically completed an API call! You asked for information from an external resource (your friend) and had to wait for a response.

Notice how we have to wait for certain actions to happen. That makes sense to us humans who understand conversation. But to Javascript, it’s not so intuitive.

Javascript wants to run everything one after the other without waiting. That leads to issues where Javascript will execute code when it should wait for a response. So what do we do about it?

Callbacks

The oldest pattern for handling asynchronous operations in JavaScript is the callback function:

function fetchData(callback) {
  setTimeout(() => {
    const data = "Some data from the server";
    callback(data);
  }, 2000);
}

fetchData((data) => {
  console.log(data); // Prints after 2 seconds
  console.log("Data processing complete");
});

console.log("This runs immediately!");

Frankly, this method sucks. While it does work, it can be a nightmare to deal with for more complicated systems. But is always nice to know your history so you can be grateful there are better ways.

fetchUserData(function (userData) {
  fetchUserPosts(userData.id, function (posts) {
    fetchPostComments(posts[0].id, function (comments) {
      // Welcome to callback hell!
      console.log(comments);
    });
  });
});

Async/Await

Async/Await is a set of specific keywords that you can apply to your functions and variables. For the keywords to work they have to be applied in a specific manner:

  • async is used on whatever function you want to run asynchronously
  • await is used on the functions you want the function to wait for
  • await must be used inside an async function
// We want "awaiting" to happen here in this function
async function fetchAllData() {
  try {
    // This is what we want to "await" for
    const userData = await fetch("https://example.org/users");
    const posts = await fetch("https://example.org/users/posts");
    console.log(posts);
  } catch (error) {
    console.error("Error:", error);
  }
}

// Call the async function as normal
fetchAllData();
console.log("This runs immediately!");

Notice that these calls are all on the function fetch(). fetch is the native Javascript API for requesting external resources which can also be other APIs. I’ll elaborate more on that in a bit. Just focus on await and async for now.

Like with the gameshow analogy, we can now give Javascript the intuition to wait for specific pieces of information before executing certain code. In the fetchAllData() function, Javascript will explicitly wait for each of the function calls marked await before it is marked as finished.

And that makes sense, all of these fetch() requests are like those lifelines in the gameshow: We ask an external resource for information and have to wait for it to get back to us.

Promises

Promises provide a cleaner way to handle asynchronous operations. A promise represents a value that might not be available yet but will be resolved at some point in the future. Schrödinger’s value, if you will.

A promise can be in one of three states:

  • Pending: Initial state, neither fulfilled nor rejected
  • Fulfilled: The operation completed successfully
  • Rejected: The operation failed

A promise is written as a value. To use it, you can either store it in a variable and return that or just return the value directly. We write our promise code on what to execute while pending and then handle what the resulting value for each state should be.

function fetchData(url) {
  return new Promise(function (resolve, reject) {
    // Use an asynchronous operation, in this case fetch
    fetch(url)
      .then(function (response) {
        // This determines if our promise rejects or resolves
        if (!response.ok) {
          // Tell our promise to enter the reject state with the value of an Error
          reject(new Error("Failed to fetch data"));
        } else {
          // Our promise resolves with the value of response
          resolve(response);
        }
      })
      .catch(function (error) {
        // Another way our promise could reject
        reject(error);
      });
  });
}

fetchData("https://myapi.com/data")
  .then((data) => {
    console.log(data); //
    return "Processed " + data;
  })
  .then((processedData) => {
    console.log(processedData);
  })
  .catch((error) => {
    console.error("Error:", error);
  });

console.log("This runs immediately!");

The beauty of promises is that they can be chained and sequenced using the .then() function. This avoids the nested structure of callbacks and makes it more linear.

fetch() returns a promise with the result of whatever you fetched or an error. This is why we can call .then() on it in our promise.

fetchUserData()
  .then((userData) => fetchUserPosts(userData.id))
  .then((posts) => fetchPostComments(posts[0].id))
  .then((comments) => {
    console.log(comments);
  })
  .catch((error) => {
    console.error("Error:", error);
  });

So that handles properly getting the data, but what exactly is an API?

What are APIs?