Demystifying Javascript - Event loop, call stack, and callback queue

Demystifying Javascript - Event loop, call stack, and callback queue

You must have heard that Javascript is a single-threaded non-blocking asynchronous concurrent language. Today we will find out what it actually means.

  1. First, let us understand Why do we call Javascript a single-threaded programming language? In most simple terms, it means that it has a single call stack. A call stack is nothing but a data structure that keeps track of function calls. Let us take an example to understand.
const foo = () => {
    console.log('foo');
}
const baz = () => {
    foo();
}
const bar = () => {
    baz();
}

bar();

// Call stack
// ----------
// foo()
// baz()
// bar()
  1. Let's understand the flow of the program in the call stack in a stepwise manner.

    1. bar() is called and added to the call stack, thereafter,

    2. baz() is added to the call stack.

    3. foo() is added to the call stack.

    4. console.log is added to the call stack, prints out 'foo', and then pops out.

    5. foo() pops out of the call stack.

    6. baz() pops out of the call stack.

    7. bar() pops out of the call stack and the program is complete.

  2. Now that, we have a basic understanding of call stack. let's move on to our next keyword i.e. non-blocking. Since Javascript has only one call stack or single thread, what happens when let's say a very slow function is running? it will effectively block the flow of the program which will make the overall experience damn slow. To understand it, let's look at a simple example.

function bar() {
    console.log('bar');
}
function baz() {
    setTimeout(() => {
        console.log('baz');
    }, 5000);
    bar();
}
function foo() {
    baz();
}
foo();

// Call Stack
// bar()
// baz()
// foo()

Now let's try to understand the constraint that comes with a single call stack. let's assume for a second, that setTimeout() is a synchronous function, then the function baz() will block the program for 5 seconds.

To avoid this sort of blocking, Javascript provides asynchronous callbacks. In the last example, we have assumed that setTimeout() is a synchronous function but in reality, it is asynchronous which means that it does not have to run in order and it does not hog the call stacks until it's completed.

  1. To understand how Javascript implements asynchronous callback, we have to understand the event loop, webAPIs, and callback queue. Before we analyze the example provided below and try to visualize step by step how the event loop and callback queue work along with javascript runtime, let us define what these are

    1. callback queue - It's a simple data structure that operates on FIFO i.e. First in First out basis. When an asynchronous function such as setTimeout() is executed, the callback is pushed to the callback queue which is then handled by the event loop.

    2. Event loop - This is the simplest piece of the puzzle. Its only job is to look at the call stack and callback queue and if the stack is empty then it takes the first thing from the queue and pushes it to the stack.

    3. Web APIs - These are high-level constructs, which are built on top of the core Javascript language, providing its users' extra capabilities. eg. setTimeout(), setInterval(), fetch() etc.

Now, look at the following example and track all the steps one by one.

function loadPage() {
    console.log('Loading webpage');
    handleBtn();
    console.log('webpage loaded');
}

function handleBtn() {
    setTimeout(function() {
        console.log('button clicked');
    }, 2000);
}

loadPage();
  1. loadPage() is added to the call stack.

  2. console.log() is added to the call stack and then 'Loading webpage' is logged on the console. console.log pops out of the stack.

  3. handleBtn() is added to the call stack.

  4. setTimeout() is added to the call stack.

  5. setTimeout() is removed from the call stack and handled by webapi, then handleBtn() pops out of the stack.

  6. console.log() is added to the call stack and 'webpage loaded' is logged on the console and then it pops out of the stack.

  7. After completion of 2 seconds, the setTimeout callback is added to the callback queue, also known as the task queue or message queue.

  8. When the call stack is empty then, the event loop takes the callback from the callback queue and pushes it to the call stack.

  9. console.log() is added to the call stack and then the 'button clicked' is logged on the console, after which it pops out of the stack.

Through event loop and callback queue, Javascript runtime does not have to wait for any asynchronous function to complete. It can figuratively jump to the next line of code and continue running till the stack is empty.


I hope that this makes you feel a bit more comfortable with the inner working of Javascript. It's natural to feel overwhelmed when learning new things. With time, you will surely understand and appreciate the nuances of Javascript. As always, you are also free to reach out to me if you have any questions!