DEV Community

Cover image for The Evolution of Asynchronicity in JavaScript: From Callbacks to Promises

Posted on

The Evolution of Asynchronicity in JavaScript: From Callbacks to Promises

JavaScript is single-threaded (one command runs at a time) and Synchronously executed(each line is run in the order the code appears). In ES6, one of the most notable enhancements was the introduction of Promises. However, to fully appreciate the significance of Promises, it's crucial to take a step back and explore the mechanisms JavaScript relied on for managing asynchronous behavior before ES6.

Let's take a simple code for example to understand javascript single-threaded synchronous behavior.

Javascript Asynchronus behaviour

This simple diagram describes the Global Memory, Execution Context, and Call Stack of JavaScript. Let's see how this code is being executed

Javascript Asynchronus behaviour

Global Memory

  1. The code starts with declaring a constant named num with the value 3. This is stored in memory for later use.
  2. Next, a function named multiplyBy2 is declared. This function takes a single argument, inputNumber.

Execution Context

  1. The function multiplyBy2 is then invoked with num (which is 3) as the argument. The function calculates 3 * 2, which equals 6, and this value is returned from the function. The returned value, 6, is then assigned to a new constant named output.
  2. The function multiplyBy2 is invoked again, this time with the literal value 10 as the argument. The function calculates 10 * 2, resulting in 20. This new result, 20, is assigned to another constant named newOutput.

Call Stack

  1. Start - Call stack is empty.(Global Function is running behind the scenes)
  2. const output = multiplyBy2(num);
    • multiplyBy2 is added to call stack.
    • multiplyBy2 is executed and removed from the call stack.
  3. const newOutput = multiplyBy2(10);
    • multiplyBy2 is added to the call stack again.
    • multiplyBy2 is executed and removed from the call stack.
  4. End - The call stack is empty again.

This describes the synchronous behavior of JavaScript, but what happens when we put an asynchronous code like calling an API using fetch or using setTimeout to delay some functionality inside the program, How does JavaScript manage these operations alongside its regular, line-by-line execution?

Let's take another coding example but we will add a setTimeout inside the code

Image description

In this example, we have used setTimeout.setTimeout is a built-in JavaScript function that delays a specified function's execution by a given number of milliseconds.

From what we have understood so far is JavaScript works synchronously means each line is run in the order the code appears and as Javascript is single-threaded,it runs one command runs at a time, so from this, we can say that the output should be:

“Me first”

while executing the code, setTimout will delay for 1000 seconds and execute printHello and then it will print “Me first”. At least from our understanding so far that's how it should be.

Let's see The output of this code:

Diagram of SetTimeout function

“Me First” is printed at first and then “Hello” is printed. It's quite a surprise, right? Isn't it breaking the rule of JavaScript?

Before I answer this question, let's talk about what are the challenges if we abide by the rules of JavaScript, let's take a real-life example

what if we are Accessing Twitter’s server to get new tweets that take a long time and then Code we want to run using those tweets

Challenge: We want to wait for the tweets to be stored in tweets so that they’re there
to run displayTweets - but no code can run in the meantime

So that's the problem we are facing here, the code can be stuck for some time before it starts executing, but as we saw from the output the previous code JavaScript is not working in this way, so what going on inside?

To grasp this concept, let's simplify our understanding of JavaScript and its relationship with the browser,

Think of JavaScript as a chef who's great at making dishes but relies on the kitchen's appliances to cook anything. Similarly, JavaScript can't do much on its own; it depends on the browser to handle tasks like logging messages to the console or making requests to APIs. In essence, a lot of what we do in JavaScript involves using the browser's capabilities, which means we're not just working with JavaScript alone.

Let's go back to our previous example involving the setTimeout function. You can think of setTimeout as a kind of request form that JavaScript fills out to ask the browser's timer to wait a specified amount of time before doing something. Once JavaScript submits this request, its job is done, and it moves on to the next task. This is why, when we used setTimeout to display a message after a delay, our code continued running and displayed other messages first. The setTimeout function is essentially just a way for JavaScript to ask the browser to handle timing, while JavaScript itself keeps moving forward.

Consider the scenario where we used setTimeout(printHello, 1000). Here, JavaScript's only role was to hand off the timing task to the browser's timer using the setTimeout function. Once that's done, JavaScript moves on, which is why “Me first!” was printed at first, and then “Hello” was printed. Take a look at this diagram, you will have a better understanding.

Image description

Now Let us take another example and trust me it will boggle your mind,

Diagram of SetTimeout function

what do you think is the output of the code? Let's take a look at it,

Diagram of SetTimeout function

Again! But we set setTimeout to 0ms, so shouldn't it execute the right way and print "Hello", what's happening here?

To understand this we have to know about another concept called “Event Loop”. we will understand it as simply as possible.

We’re interacting with a world outside of JavaScript now - so
we need rules. rules that will help us clearly understand what is happening while JavaScript is trying to interact with a web browser.

So let's take a look at another code

Diagram of SetTimeout function

Now let's make a diagram of the executed code:

Diagram of SetTimeout function

After executing setTimout at 0ms, it should go to the callstack instantly but as we can see callstack is empty! and in the next execution, it will execute the function blockFor1Sec() and put it into call Stack. In the previous example, we saw something similar that setTimeout was set to 0ms but it didn’t print out print hello instantly but executed the next line first,
so this raises two questions,

  1. Why even after completing in 0 ms didn’t go to the callStack instantly?

  2. After how many times it will go to the callStack and get executed?

I have a task for you to try out. In the previous code, there is an empty function called blockfor1Sec. Now put a for loop that will run for say 3 million times “Blocker”, and observe how many seconds later the printHello function gets executed, it will give you an intuition on the second question. I would highly suggest doing this before reading the next part.

So, there is another part of the puzzle that we need to know about, it’s a queue, a queue of callbacks, so the function printHello put into setTimeout was a callBack function, (Don’t compare this callBack function with the one that are put into Higher Order Function)

So in 0ms, our printHello function is in the callBack queue, ready to run. So as we complete the setTimeout, we will go to our next line which is in 1 ms, we are gonna call the blockFor1Sec function.

So blockFor1Sec is going to the execution context and runs say for 1000ms, but printHello is waiting in the callBack queue from the 0ms so is it going to execute any time between those 1000ms? If you have completed the task you know that it didn't run between that time, so when our little function that is waiting in the callBack function would run?

So after the blockFor1Sec is done after 1001ms is the printHello function allowed to go out of the queue and allowed to execute? NO!! it’s still not allowed to do so.. Sad little function, isn't it?
after blockFor1Sec is run it will execute the next line and log “Me first!” in 10001ms, Now in 1002ms when all the code has been executed, printHello is allowed to run and put into the callStack.Yes! Finally, at 1002ms our little printHello function is grabbed out of the queue and put into the call Stack. and print Hello”!

so the question arises who is controlling the queue and deciding when to put the callback function from the queue to the call stack? and what is the strict rule it follows to do so?
So the rule is all regular code and all synchronous code has to be completed before it calls the callback function from the queue, and who is doing that is the “Event Loop”!!!
Event Loop is constantly checking if is there any callBack function in the queue and if the global() execution context is finished meaning all the regular synchronous code has done its work. If there is any function inside the queue and the global function is executed it will put the function from the queue to the callback and the function will be executed. So it allows us to be certain when our code will run, we don’t know when but we know the order of the execution.

Diagram of Event Loop

So folk that was before EC6, the entire modal, of how asynchronous code runs inside javascript, but after ES6 came with promises, it changed everything and gave us a much more meaningful way to handle the asynchronous behavior of javascript. we will talk about it in our next post.

Thanks for reading, see you in another thought-provoking JavaScript concept! This post is inspired by Will Sentance's course JavaScript: The Hard Parts, v2. If you have any questions you can ask me in the comment section below

Top comments (0)