Asynchronous JavaScript Explained

Raz Gaon
The Startup
Published in
6 min readMay 14, 2020

This article will explain the non-blocking nature of JavaScript and what patterns we as programmers can implement. The explanations will be complemented with short stories used as analogies. Enjoy!

Three months ago, I went to a restaurant. All the waiters were standing by the kitchen without doing anything. After a few minutes, a waiter took my order. After submitting my order he just waited without approaching other customers. Only after delivering me my pizza, he finally walked away to serve another table. All four waiters in that restaurant acted in the same peculiar way. Most of the customers were waiting to order but there were no available waiters to serve them. Open orders blocked the restaurant's ability to serve customers.

This behavior is known as blocking behavior.

Photo by Alan Hardman on Unsplash

Our real experience in restaurants is different. We order from a waiter, the waiter sends the order to the kitchen and then moves on to take orders from other tables. When the food is ready, the kitchen notifies an available waiter to take the food to you. This is known as non-blocking behavior.

Why Do We Need Non-Blocking Code?

JavaScript is different from other programming languages because it can’t wait for events to finish. Let’s say a user clicked on a subscribe button on Youtube. Should Youtube pause and wait for a response from the server? If so, the user won’t be able to browse comments, watch the video, or do any other action on the website. Some websites have functions that can take 15 seconds — a long time to keep a user waiting. In order to handle this issue, JavaScript had to be implemented in a non-blocking asynchronous fashion. There are 3 different forms of non-blocking code in JavaScript:

  1. Callbacks
  2. Promises
  3. Async/Await

Callbacks

In JavaScript, functions are objects, hence a function can take another function as an argument.

A callback is a function that is executed inside another function once some behavior completes. Let’s work with the restaurant example: I have a function that is called cookMeal. The function receives a callback function as an argument that is called serveMeal. The function serveMeal is automatically executed once cookMeal is done.

So what is happening here? We have two functions: one for cooking the meal(the kitchen is responsible for doing kitchen things) and one for serving the meal(an available waiter takes the meal to the customer). What is the callback argument sent to cookMeal? It’s a function that will execute with the finished meal as its argument when cookMeal completes.

We told our program to execute the function serveMeal once the function cookMeal is finished. We make sure that serveMeal happens only after the meal is cooked, but, without blocking the program’s execution. This pattern is called the callback pattern and has many flaws, the biggest one being too many layers of callbacks — commonly known as “callback hell”.

@peterchang_82818

Promises

The 6th edition of ECMAScript, initially known as ECMAScript 6 (ES6), later renamed to ECMAScript 2015, was finalized in June 2015. This version introduced promises — a new way to handle asynchronous tasks.

A promise is used for the same use cases as a callback — handling code with unknown completion time without blocking the program. For example, fetching data from an API that can take x seconds to respond.

To explain the terminology of promises I will use another analogy. Mr. Beast is one of the best YouTubers out there. As an avid fan, I want to know when he releases his next video. How can I do that? I click on the subscribe button on his channel. Youtube “promises” me that when the video will come out, they will update me, and if the video won’t come out for any reason, they will send me a notice that a problem occurred. I continue with my life, and when the video is released, Youtube notifies me and I watch the video.

Shot by Business Insider

Diving deep

A Promise takes as an argument a function called “executor”. The executor runs when a new Promise is created. The executor has two parameters: resolve — a callback that is called if the function executed successfully, and reject a callback that is called if an error occurred.

When the executor function finishes its tasks, it calls one of the callbacks: resolve(value) or reject(error)

The return value of a new Promise is a “promise” object. The object has 3 states: pending (the executor hasn’t finished), fulfilled (resolved was called), or rejected (reject was called).

Mr.Beast example);

In the example, we can see that I subscribed to Mr.Beast’s next video. The video has been uploaded and then Youtube updates the user. If there was a problem in uploading the video, Youtube catches the error and notifies the user. The user is not updated until after the video has finished uploading (or has had an error).

***

It is important to note that instead of callback hell, we can chain promises. If we want to execute a series of asynchronous functions sequentially, we chain them. More reading.

***

Async/Await

ES8 (2018) introduced a new syntax to work with promises: “async/await”. The main improvement of the new syntax is revamping the way we write asynchronous code.

“In computer science, syntactic sugar is syntax within a programming language that is designed to make things easier to read or to express. It makes the language “sweeter” for human use: things can be expressed more clearly, more concisely, or in an alternative style that some may prefer.” — Wikipedia

Async/await is actually just syntax sugar built on top of promises. It allows us to write asynchronous code in the same fashion we write synchronous code. The word “async” before a function means one simple thing: the function will return a promise and allow us to await other asynchronous functions inside. For instance, the following function returns a resolved promise with the result of “Fun”. let’s test it:

async function haveFun() {
return "Fun!";
}
haveFun().then((result) => console.log(result)); // "Fun"

Using async transforms our return value to a promise. The value we return from our function can be consumed just like any promise value and the function doesn’t block the main thread.

Now, what about the “await” keyword? Await is the equivalent of using .then on an async promise. It waits until the promise is either fulfilled or rejected.

Here is our previous example in the new syntax:

uploadVideo() {
// When the video is done,
const video = ...
return new Promise((resolve, reject) => {
// Video related stuff - can take time
try {
resolve(video);
} catch (err) {
reject(err);
}
})
}
async subscribe(){
try{
const video = await uploadVideo();
notifyUserOfNewVideo(video);
}catch(err){
notifyUserOfError(err);
}
}
subscribe();

* Side note: await can only be used inside async functions!

As you can see, instead of chaining a .thento our promise, we await it. If there was an error, our code catches it just like before! It is important to understand that inside our function, the code seems as if it is executed “synchronously” and is blocking the thread, but it isn’t because the function itself returns a promise which is non-blocking.

Conclusion

In this article, we talked about the asynchronous nature of JavaScript and learned about the various ways we can implement asynchronous behavior. We didn’t dive deep into the implementation, but I hope it helped you understand the basic concepts behind each method.

Thank you for reading!

--

--

Raz Gaon
The Startup

Software Developer, Student at MIT, passionate about web development, writing, and boxing.