Javascript Async Fundamentals

Photo of Michał Sobczak

Michał Sobczak

Jul 1, 2020 • 11 min read
macbook pro

Picking up from my article about asynchronicity and threading in programming, let’s continue the topic using the example of JavaScript.

For more about these topics and how these two mechanisms work I encourage you to take a look at the first part here.

As most of you know, JS does not support multi-threading out of the box. For those who don’t want to go deep into this or forget the concept, here’s a quick refresher. JavaScript, being a single threaded language, is technically only capable of executing one line of code after another. While this is basically a good thing, as your program will be super predictable, it might have serious caveats e.g. when code responsible for rendering UI will have to wait for some data before providing further functionality, resulting in a frozen interface (users won’t be happy about it). On the other hand, there are programming languages created to support execution of multiple processes at the same time, but those are vulnerable to known complexity issues such as deadlocks or races over resources.

However, you probably don’t see many frozen UIs that force you to wait after every action, do you? 🤔 Didn’t we just learn that JS is a single threaded language? How is this possible? What mechanisms does JavaScript use to make it all look smooth? Well, let’s take a look at how it works under the hood!

Javascript engine and the runtime

When you lift a car’s hood up, you typically see the engine. The same goes for Google Chrome or Node.js - they use the nice and powerful V8 engine. It is written in C++ and maintained by Google. You can read about V8 in detail here (How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code). V8 consists of two main components - the memory heap and the call stack.

The memory heap is nothing more than an unspecified place in the computer's memory, used to store different variables declared in the code.

The call stack is a code interpreter mechanism that is responsible for the correct order of execution. In other words, the interpreter uses the call stack to know at which line the program is currently running. JavaScript, being a single threaded language, has only one call stack where all called functions are stacked with the uppermost one being presently executed.

But today’s applications use more advanced technologies than just saving variables in memory and executing code line by line. Outside of engine there are a few other things such as Web APIs, event loops, and callback queues. Keep in mind that without those, there wouldn’t be any possibility to simulate multitasking in JS.

First off, there are Web APIs, such as DOM, Fetch, Service Workers, Web Audio, and many, many more. Basically these are interfaces built into the browser (but not into the engine) that help or enable developers to create much more complex and sophisticated applications without implementing them from scratch.

The next important thing of the whole runtime is mechanism called event loop, which “connects” callback queue with JS Engine (in Node.js event loop is actually a part of engine). In other words event loop just provides further code to be put on top of the call stack and executed by the engine.

On our way to understanding how the things listed above work together let’s try to answer an important question. How does Javascript simulate the feeling of multi-threading, resulting in smooth user interfaces and non-blocking behaviour?

Asynchronicity in JS

Let’s dissect a part of a typical user interface in a website with few tabs, where the ‘Load more’ button is implemented at the bottom of an image gallery. Whenever the user clicks the button more images are loaded and rendered to the screen. Without the support of mechanisms that exist outside of the engine (Web APIs, callback queue, event loop), requests for more images would block other parts of code to be executed as JS uses the run-to-completion paradigm, meaning every other user interaction would need to wait for its turn.

Fortunately the whole runtime is there to prevent such things. When the user clicks the button, the event handler is called and added to the call stack for the execution. The body of this function is an AJAX request to fetch the needed portion of images; it also prints a message to the console using console.log(‘Loading started’).

HTML markup

<button id=”fetch-button”>Fetch images</button>

JavaScript file

const button = document.getElementById(‘fetch-button’);
button.addEventListener(‘click’, handleClick);

const handleClick = () => {
  console.log(‘Loading started’);
  fetchImages(‘https://awesome-gallery.com/api/images’, handleLoadedImages);
}

There are a few things happening here. The pictures below should be very helpful to get an understanding of this code. Read the text below while following the flow.

The JS engine executes the code line by line, so the first important thing that will happen is adding an event listener to the button element.

4-34

The function call appears in the call stack and the Event Listener Web API is used to register click events. Technically, the code execution goes further down, but the runtime needs to know what to do when the user actually clicks the button (this is the asynchronous part of the code, as it might happen later in time). To do that, asynchronous functions have the possibility to add so-called callback functions - which are instructions on what to do after the first part is done.

3-39

When the button is clicked, the registered Event Listener will intercept the event and execute its callback. In this case the callback will be of course our handleClick() function. Since callbacks are in the asynchronous part of runtime, they can’t be added directly to the call stack and executed immediately.

This is where the callback queue and event loop come in.

The handleClick() callback will first go to the callback queue waiting for its turn. If there is nothing preceding it, the event loop will push that function to the top of the call stack to be executed by the engine. In the body of said function resides another asynchronous piece of code - fetchImages().

2-51

Now console.log(‘Loading started’) will go directly to the call stack and get executed. Immediately after that, fetchImages() lands in there, but execution of this code to full completion might block the interface. In fact, it is delegated to the AJAX Web API with callback(handleLoadedImages()) to perform a request.

1-51

As the handleClick() and fetchImages() are both off the stack, the user is still able to perform actions on the website and doesn’t have to wait for response with a frozen UI - cool, isn’t it?

In the background downloading images is in progress. When completed, the callback function is added to the callback queue, waiting its turn to be picked up by the event loop and put into the call stack. In our case when handleLoadedImages() gets to be executed it might for example render images to the user view.

Callbacks themselves

Callbacks are an essential part of writing asynchronous code and every developer should get at least a general understanding on how those work. Large parts of the existing websites are developed using those fundamental mechanisms.

One dangerous thing about callbacks is that they can have their own callbacks. This means that you can get into some trouble when the stack of callbacks gets much bigger than the example above. Consider the following code:

handleClick(x, function() {
  console.log(x)
  // more code...

  setTimeout(function() {
    //more code...

    ajax('https://test-url.com/resource', function(x) {
      if(x === 200) {
        ajax('https://test-url.com/additional-resource', function(){
          // instructions what to do with resource
        })
      } else {
        // other code
      }

    })
  }, 1000)
})

As you can see this might lead to code that is hard to understand and maintain. First, handleClick needs to be called, then it invokes its callback, which then will set up a timer after which the AJAX request is sent. After all of that we get to decide what to do with the response. Some might say that you could extract the callbacks to named functions to make it more clear in the main block, but then you would have to follow the code and check for isolated instructions in different places. Still not too convenient.

Also, what if there was an error in a random part of this callback chain> You would have to put in extra code to catch all possible outcomes, which could quickly pile up to a huge number of lines, causing this to be less and less readable. Preventing such hard to maintain code can be achieved in various ways using more up to date features, such as Promises and async/await.

Summary

In this part we’ve tackled the runtime of JS - in which order the code gets executed and how each element of it helps do it. In short, tasks are pushed onto the call stack and executed. Being asynchronous, they are delegated to the proper Web API, where they wait to be finished. Next, the corresponding callback lands in the callback queue waiting its turn to be pushed by the event loop to the call stack, where it will eventually get executed.

There is a great app created by @philip_roberts, called Loupe (http://latentflip.com/loupe/) where you can freely explore and play with JS async code, while seeing what happens under the hood.

Asynchronicity is a very important topic in web development as it helps achieve non-blockable, smooth interfaces and user experience. It’s a great mechanism that every developer should be aware of. However, with great power comes great responsibility. While trying to achieve the best looking interface for your user, also think about how to make the best looking code for other developers (including you in the future).

In the final part of my series on asynchronous code in JS, we will go through modern patterns with examples of asynchronous code that can be easily understood, such as Promises and async/await.

Stay safe!

Photo of Michał Sobczak

More posts by this author

Michał Sobczak

Michał is a positive person who always finds a bright side to any situation. From a very young age,...
How to build products fast?  We've just answered the question in our Digital Acceleration Editorial  Sign up to get access

We're Netguru!

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency
Let's talk business!

Trusted by:

  • Vector-5
  • Babbel logo
  • Merc logo
  • Ikea logo
  • Volkswagen logo
  • UBS_Home