Event Loop, Callbacks, Promises, async-await in JavaScript : Part 1

Event Loop, Callbacks, Promises, async-await in JavaScript : Part 1

JavaScript Deep Dive

Introduction

JavaScript, the language of the web, provides developers with powerful tools to create interactive and dynamic web applications. However, its single-threaded nature can sometimes lead to confusion when dealing with concurrent tasks. In this blog, we'll delve into the JavaScript event loop, understand how it handles concurrency, and explore techniques to optimize performance. Let's unravel the mystery of the event loop.

Call Stack and Execution Contexts

JavaScript keeps track of the currently running task or function in a call stack. A new execution context is created with each function call, containing variables, arguments, and the code position. Functions are pushed onto the stack as they are executed, and when they are finished, they are popped off the stack. For example,

function f1(){
    function f2(){
        function f3(){
        }
        f3();
    }
    f2();
}
f1();
// call stack => f1 - f2 - f3 <- HEAD

As you might be aware how elements get added and popped from a stack by LIFO (Last In First Out) mechanism. So the functions will be popped in f3 -> f2 -> f1 order.

Synchronous Code Execution: Problems and Solutions

This is the synchronous way of handling execution which means the code is executed sequentially one thing at a time without caring if some tasks are slow.

As you might have already guessed that this is not the right way to execute a program efficiently. There should be a way to execute a piece of code without this Blocking behavior (Slow processes block the execution of other code).

A solution can be that we take the slow code and execute it in some thread other than the main thread so that the rest of the tasks can be executed without any waiting time. But the V8 engine (JavaScript runtime) is single threaded meaning it can do only one task at a time, so how can we achieve this?

Have you heard about setTimeout() function? If not it is simply a function that takes in another function and a time in milliseconds as its parameter and the function gets executed after a specified time. Yes! it is non-blocking. But how can it run in parallel when we have only one thread? Turns out that this setTimeout() is not contained in V8 and it is provided by the environment that hosts V8 which is the Browser or Node (if you are running on the server side)!

Asynchrony and Callback Queues

The relationship between the now and later parts of your program is at the heart of asynchronous programming. In the example above, we take the function inside setTimeout() stack frame out of the Callstack and move it somewhere so that it can be executed later after the specified time. This waiting for a specified time is handled by the host in another thread than the main thread.

After x milliseconds the callback function will be added to the Task queue by the host. When the Callstack is empty callbacks in the Task queue will be dequeued and added to the stack to execute. Dequeuing and executing from the Task queue is non-blocking to adding new tasks to the Callstack, so if new items are being added to the Callstack the Task queue waits for the Callstack to be empty.

Let us look at some examples to understand this better,

  1. When we execute the code below the event loop executes the while loop and it will never finish. You can see that the screen won't respond to anything you do. This is because the event loop is busy executing the task below which never finishes and the task you are entering is queued behind the below code which will never be reached.
document.querySelector("button").addEventListener("click", () => {
  while (true);
});
  1. Guess what happens when you click on the button for the code below. After clicking you can see that the screen is responsive but just for 5 seconds. Because when encountering setTimeout() the host will wait for 5 seconds and since it is non-blocking other stuff gets executed on the waiting time. After 5 seconds the callback gets pushed into the Task Queue and since the Callstack is empty, the while loop gets executed and this blocks everything.
document.querySelector("button").addEventListener("click", () => {
  setTimeout(() => {
    while (true);
  }, 5000);
});
  1. The code below will be a bit tricky to understand if you are new to this. When clicking the button the browser waits for a specified time but here it's zero seconds, what? Isn't it just the same as synchronous execution without setTimeout()? One thing to note here is that it is not really 0 and it is 4 ms, remember the function inside the setTimeout() will be moved to the Task queue no matter what happens. So the tasks in the Task queue won't be dequeued unless the Callstack is empty. So we can say that using setTimeout(..., 0) is a hacky way for asynchronous execution. However executing the below code even though there is infinite setTimeout() calls recursively, is non-blocking. Try to think why!
document.querySelector(".button").addEventListener("click", () => {
  function sample() {
    setTimeout(() => {
      foo();
    }, 0);
  }
  foo();
});

Any problems?

There are a few drawbacks to callbacks that limit their usage to every asynchronous operation. The most important are listed below,

  • When dealing with multiple asynchronous operations, the use of callbacks can lead to deeply nested code structures, making the code hard to read and maintain. This is called the Callback Hell.

  • When the callback is handled by a third party, we call out the function and trust them that they will call the callbacks properly. This Inversion of Control can lead to trust issues.

  • When multiple asynchronous operations are initiated and modify the shared data, the order of their completion and the resulting values can be non-deterministic (Race Condition). This can lead to conflicts and inconsistent states if the operations are not properly synchronized.

  • Error handling becomes cumbersome with callbacks, as you need to pass error parameters along with the success results. This can result in repetitive error-checking code within each callback.

Conclusion

Understanding the event loop and how JavaScript handles concurrency is crucial for writing efficient and responsive web applications. By leveraging the event loop, developers can harness the power of asynchronous programming, handle events effectively, and optimize performance. Embrace the event loop, and unlock the true potential of JavaScript!

I hope this blog post helps you gain a deeper understanding of the event loop and its role in JavaScript concurrency. The future blogs will be on Improving user experience by mitigating the impact of resource-intensive tasks, Promises in Javascript and the new async-await.

Happy coding!