DEV Community

Cover image for Asynchronous Javascript - 03 - The Callback Queue
Kabir Nazir
Kabir Nazir

Posted on • Updated on

Asynchronous Javascript - 03 - The Callback Queue

We had discussed the workings of the single-threaded execution and the call stack of Javascript in the previous articles. We gained an understanding of the way synchronous functions are executed in Javascript. In this article, we will actually start looking at how asynchronous functions operate and are placed in the order of execution in Javascript.

When we are asked to think of one of the simplest functions that are asynchronous in Javascript, most of us would come up with the builtin setTimeout function. Let’s look at a simple example

The above code prints “Hello” onto the console after a delay of 1000 milliseconds (1 second). Sounds simple enough, right? Let us now tweak the code a bit.

The above code will print “Hello” onto the console after a delay of 0 seconds. This means that it will print it instantly. How about we add some code after the setTimeout function?

The above code should print out “Hello” and then print “World”, right? From what we have seen about the call stack, the setTimeout function at line 1 is supposed to go into the call stack first, followed by the console.log function at line 5. But let’s look at the actual output

    Output:
    World
    Hello
Enter fullscreen mode Exit fullscreen mode

We see that “World” is printed before “Hello”. This means that the console statement at line 5 got executed before the setTimeout function. How is that possible? It’s possible because the setTimeout function never went into the call stack. Only the console.log statement at line 5 was sent into the call stack and got executed.

But we see that the setTimeout function too eventually got executed. This is because the setTimeout function got passed into something that’s called a callback queue in Javascript.

Callback queue

Before we look into the callback queue, let’s understand a few things about the setTimeout function. The first thing we need to know that setTimeout is not part of Javascript. It’s not found in the ECMAScript specs or is part of the Javascript engine. This function is actually provided by the web browser that Javascript runs on. To be more precise, it’s part of the window object in the browser. Hence, the setTimeout function will run normally on a browser but will not work on other environments of Javascript like Node. There are other functions like setTimeout which are part of the browser but not Javascript itself, like console (to print logs), document (to access elements of HTML), localStorage (which allows saving key/value pairs in the browser’s memory) and so on.

When an asynchronous function like setTimeout gets called, it doesn’t get added to the call stack. It instead gets added to the callback queue. The callback queue, as the name suggests, is a queue. Hence, functions added to it are processed in a first-in-first-out order. When the event loop in Javascript is fired, it first checks the call stack to see if it’s non-empty. If so, it executes the function at the top of the stack. However, if it finds the call stack to be empty, the program continues on with its execution. Once the end of the program is reached and the event loop is fired, as usual, it first checks the call stack to see if it's non-empty. If it’s not, it starts executing the functions one by one from the top of the stack. Once the call stack is empty, the event loop then checks the callback queue to see if it’s non-empty as well. If yes, it then proceeds to execute the functions one by one in the queue, starting from its head. Keep in mind that the functions in the callback queue start getting executed only after

  1. We have reached the end of the program

  2. There are no functions left to be executed in the call stack

The above flow might sound a bit confusing to grasp at first. Let us try to understand it better with the help of an example.

In the above code, we have created a function blockThreadFor1Sec. Let us assume that it contains some code that takes approximately 1 second to run, for e.g. a for loop that is looped a billion times. When the loop finishes, the function then prints “1 second elapsed” onto the console.

At the beginning of the program, both the call stack and the callback queue are empty. Let us also take note of the timestamp at each step. Currently, it is at 0 ms

    Timestamp: 0 ms

    |               |
    |               |
    |               |
    |               |
    |               |
    |_______________|

       Call stack

    |               |
    |               |  
    |               |
    |               |
    |               |
    |               |

      Callback queue
Enter fullscreen mode Exit fullscreen mode

In line 1, the program only defines the function block1Second. The program then moves to line 6, where let’s say we’re at a timestamp of 1 ms (this isn’t the accurate timestamp, but just a rough value we take for simplicity). The program calls the setTimeout function and since it’s an asynchronous function, Javascript puts this function into the callback queue.

    Timestamp: 1 ms

    |               |
    |               |
    |               |
    |               |
    |               |
    |_______________|

       Call stack

    |               |
    |               |  
    |               |
    |               |
    | setTimeout()  |
    |               |

      Callback queue
Enter fullscreen mode Exit fullscreen mode

When the event loop is fired, it sees that the call stack is empty. It then looks at the callback queue and finds it non-empty with the setTimeout function at the head. But it doesn’t immediately execute it because the function is set to execute only after a delay of 1000 ms. So, in our case, the function is to be executed only at a timestamp of (1 + 1000) = 1001 ms. Hence, the code inside the setTimeout function isn’t called yet.

The program then moves to line 10, at which point let’s say we’re at a timestamp of 2 ms. The block1Second function is called and since it’s a normal synchronous function, it is added onto the call stack.

    Timestamp: 2 ms

    |               |
    |               |
    |               |
    |               |
    | block1Second()|
    |_______________|

       Call stack

    |               |
    |               |  
    |               |
    |               |    Scheduled to
    | setTimeout()  | -> execute at
    |               |    1001 ms

      Callback queue
Enter fullscreen mode Exit fullscreen mode

When the event loop gets fired, it sees that the call stack is non-empty. Hence, it executes the function at the top of the stack, which is block1Second. This function would take approximately 1 second or 1000 milliseconds to execute. Hence, when its execution is finished, we should be at a timestamp of (2 + 1000) = 1002 ms.

Here’s where things get interesting. As we have seen before, the setTimeout function was scheduled to be executed at a timestamp of 1001 ms. So, when the event loop is fired at a timestamp of 1001 ms, the setTimeout function present in the callback queue is not called yet because of condition #2 mentioned above that needs to be fulfilled first. i.e. the call stack needs to be empty. The call stack becomes empty only at 1002 ms when the block1Second function has finished executing and is removed from the call stack.

Let us now look at what happens at a timestamp of 1002 ms. The block1Second function finishes executing, “1 second elapsed” gets printed onto the console and the function is removed from the call stack.

    Timestamp: 1002 ms

    |               |
    |               |
    |               |
    |               |
    |               |
    |_______________|

       Call stack

    |               |
    |               |  
    |               |
    |               |    Scheduled to
    | setTimeout()  | -> execute at
    |               |    1001 ms

      Callback queue
Enter fullscreen mode Exit fullscreen mode

Now that the call stack is empty, one might assume that the setTimeout function is ready to be called the next time the event loop is fired. However, that is not the case as condition #1 mentioned above has not been fulfilled. i.e. we haven’t reached the end of the program yet. Hence, the program moves on in its execution without executing the setTimeout function.

At line 12, we’re at a timestamp of 1003 ms. The program calls the console.log statement, which gets added to the call stack since it’s synchronous.

    Timestamp: 1003 ms

    |               |
    |               |
    |               |
    |               |
    | console.log() |
    |_______________|

       Call stack

    |               |
    |               |  
    |               |
    |               |    Scheduled to
    | setTimeout()  | -> execute at
    |               |    1001 ms

      Callback queue
Enter fullscreen mode Exit fullscreen mode

When the event loop is triggered, it sees that the call stack is non-empty with a single function. Hence, the console.log function is executed (which prints “World” onto the console) and then removed from the call stack. We have now reached the end of the program and are at a timestamp of 1004 ms.

    Timestamp: 1004 ms

    |               |
    |               |
    |               |
    |               |
    |               |
    |_______________|

       Call stack

    |               |
    |               |  
    |               |
    |               |    Scheduled to
    | setTimeout()  | -> execute at
    |               |    1001 ms

      Callback queue
Enter fullscreen mode Exit fullscreen mode

When the event loop is now triggered, it sees that the call stack is empty. It also sees that the end of the program has been reached. Now that both the conditions have been fulfilled, the event loop finally is ready to move on to the callback queue to start executing functions from there. It sees that the callback queue is non-empty. Hence, it executes the function at the head of the queue, which is our setTimeout function. The function prints “Hello” onto the console, after which the function reaches its end of execution and is removed from the callback queue.

    Timestamp: 1005 ms

    |               |
    |               |
    |               |
    |               |
    |               |
    |_______________|

       Call stack

    |               |
    |               |  
    |               |
    |               |
    |               |
    |               |

      Callback queue
Enter fullscreen mode Exit fullscreen mode

When the event loop is triggered again, it sees that the call stack is empty, the program has reached its end and the callback queue is also empty. Hence, the program is finally terminated.

There’s just one more concept of asynchronous Javascript that we need to learn, which deals with promises and the microtask queue. We shall learn about it in the final part of this series.

This post was originally published here on Medium.

Top comments (0)