Lesson Sunday

So far, we've learned how to make a basic API call. We've also learned a little about the traditional way of handling asynchrony in JavaScript: the callback. However, once we start trying to chain multiple asynchronous pieces of code together, we run the risk of running into what's known as callback hell.

What does that even mean? Well, let's take a look at how we used a callback in our original API example. We'll take a little snippet of code from that example and see how we might chain multiple callbacks together to handle async code.

// Snippet from original code.

    request.onreadystatechange = function() {
      if (this.readyState === 4 && this.status === 200) {
        const response = JSON.parse(this.responseText);
        processRequest(response);
      }
    };

// Hypothetical functions using more callbacks.

processRequest(response) {
  // Make another API call using the response from the first API call and save it in a variable called secondApiResponse.
  crunchData(secondApiResponse);
}

crunchData(anotherResponse) {
  // Do some async operation to organize a large batch of data from an API and save it in a variable called finalData. 
  getElements(finalData)
}

getElements(data) {
  // Do something to arrange elements in the DOM for a user.
}

The example above is meant to be a little complex and confusing, so it's fine if it looks that way - that's the point. What's happening in the hypothetical code above is the following:

  • If our first request is successful, our code will trigger a callback to make a second request using the data from the first request. Specifically, it will call processRequest().
  • crunchData() can't be called until after processRequest() is called. When processRequest() is called, our code will trigger the crunchData() callback to crunch the data. This data crunching will be async, too, because this theoretical API has large data sets and it will take our application some time to crunch it.
  • Our crunchData() function will activate another callback called getElements() which finally determines how the resulting data will show up in the DOM.

Note that this snippet would also need more code to handle async operations (similar to what we do with our onreadystatechange listener) - that code is not included here.

There are quite a few problems with the code above. One of the biggest problems is that these four hypothetical functions are now reliant on each other. Make a change to one function and the entire house of cards might come tumbling down. In this example, all the resulting callbacks are in order, making them somewhat easier to read, but imagine a large codebase where there are callbacks scattered throughout different files. After all, we shouldn't store functions that make API calls with algorithms that crunch data, nor would we store either with functions that update how the DOM looks. Also, some of these functions may be called in many different places in our code - making it impossible to write the code in the order that it would actually be called. The result is that developers end up with "spaghetti code." This means that they have to follow a strand of spaghetti through many other tangled strands of spaghetti to make updates or fixes to their code. Finally, the example above is just four functions - now imagine a dozen functions, some used in just part of the codebase, some used in many places.

That is callback hell in a nutshell.

Fortunately, JavaScript has newer tools that allow us to avoid callback hell. While callbacks are an essential part of JavaScript, we don't need to rely on them to handle all our asynchronous code.

We are going to learn about two other ways JavaScript handles async code during this section:

  • Promises
  • Async functions with the await keyword

There are other tools that JavaScript uses to deal with asynchrony ranging from generators to observables but we can only cover so much in one section. For this section's independent project, you can use callbacks, promises, or async functions with an API call, but you'll most likely use promises.

This lesson will provide a brief overview of the three techniques we're learning in this section, including advantages and disadvantages of each.

Callbacks

A callback is just a function that's called by another function. They are a huge part of JavaScript and an essential tool for all developers.

Async Advantages: A callback is a simple way to handle basic asynchrony, especially if the code isn't too complex. Also, callbacks can handle just about anything so they are a great all-purpose tool.

Async Disadvantages: Callbacks quickly become difficult to work with when many are chained together. Code which uses many callbacks to handle async code is difficult to read and reason about and bugs can be very difficult to deal with, leading developers into what is known as callback hell.

Promises

Promises are a relatively new feature in JavaScript - they were originally added to ES6 (released in 2015). Promises wrap async code and are either resolved or rejected. We can use a method called Promise.prototype.then() with a promise to handle a resolved or rejected promise. We'll learn more about them in the next lesson.

Async Advantages: Promises are great and easy to work with once you understand them. So be patient with yourself if they are confusing at first. We can also write code in the order it runs and easily manage callbacks with promises. Promises are great with API calls!

Async Disadvantages: A promise can only be resolved or rejected once. If you want a piece of code that handles many async operations, a promise isn't the best way to go.

Async Functions

Async functions are even newer than promises - they were added to JavaScript in ES7 (released in 2017). We can wrap code in an async function and force it to run in order as if it were async code.

Async advantages: Because code in async functions runs in order, it's easier to read, write, and reason about. Async functions are also just a really concise, elegant way to write async code.

Async disadvantages: All of the code inside an async function will be blocking - and will run synchronously - so don't put too much code inside one because then you might start overriding JavaScript's non-blocking advantages.

So Which Tool Do I Use?

Students often get confused throughout this section. At this point, it's tempting to want to know the best tool for the job and stick with that. But there are advantages and disadvantages to each tool depending on what you are doing. We can use the analogy for carpentry tools as well. Want to attach a nail to a board? Use a hammer. How about many, many nails? Use a nail gun. How about a screw? Use a screwdriver. And many screws? Use a drill. We can't tell a carpenter just to use a hammer and forget about it. For the same reason, we can't just tell a developer to use promises and leave it at that.

However, promises are often the best way to handle API calls. That's because generally we'll make an API call just once in a function - and promises are rejected or resolved once. For that reason, most of the code you'll write during this section will probably use promises with callbacks - and this combo is a great way to go for this section's independent project as well. Just make sure you take some time to practice with each tool during the daily classwork.

Lesson 14 of 26
Last updated October 12, 2021