Synchronous and asynchronous JavaScript – call stacks, promises, etc

Let me start this article by asking “What is JavaScript?” Well, here’s the most confusing yet straightforward answer I’ve found so far:

JavaScript is a single-threaded, non-blocking, asynchronous, concurrent programming language with great flexibility.

Wait a minute — does it say both single-threaded and asynchronous? If you know what single-threading means, you probably associate it with synchronous operations. So how can JavaScript be asynchronous?

In this article, we’ll look at the synchronous and asynchronous parts of JavaScript. You use them almost every day in Web programming.

If you also enjoy learning from video content, this article is also available as a video tutorial here: 🙂

In this article, you will learn:

  • How JavaScript is synchronized.
  • How asynchronous operations happen when JavaScript is single-threaded.
  • Understanding how synchronous versus asynchronous can help you better understand the promise of JavaScript.
  • Many simple but powerful examples illustrate these concepts in detail.

JavaScript functions are first-class citizens

In JavaScript, you can create and modify functions, use them as arguments, return them from another function, and assign them to variables. All of these capabilities allow us to use functions anywhere to place a stack of code logically.

Lines of code logically organized into functions

We need to tell the JavaScript engine to execute the function by calling them. It looks something like this:

// Define a function
function f1() {
    // Do something
    // Do something again
    // Again
    // So on...
}

// Invoke the function
f1();
Copy the code

By default, each line in a function is executed sequentially, one line at a time. This is true even if you call multiple functions in your code. Again, line by line.

Synchronous JavaScript – How the function execution stack works

So what happens when you define a function and then call it? The JavaScript engine maintains a stack called function Execution stack. The purpose of the stack is to keep track of the function currently being executed. It does the following:

  • When the JavaScript engine calls a function, it adds it to the stack and execution begins.
  • If the currently executing function calls another function, the engine adds the second function to the stack and starts executing it.
  • Once it finishes executing the second function, the engine pops it off the stack.
  • Control to resume execution of the first function from the last point of departure.
  • Once the execution of the first function is complete, the engine pops it off the stack.
  • Continue in the same way until there is nothing left to put on the stack.

The function execution Stack is also called the Call Stack.

Function execution stack

Let’s look at an example of three functions executed one by one:

function f1() {
  // some code
}
function f2() {
  // some code
}
function f3() {
  // some code
}

// Invoke the functions one by one
f1();
f2();
f3();
Copy the code

Now let’s see what happens to the function execution stack:

A step-by-step process shows the order of execution

Did you see what happened there? First, f1() enters the stack, executes and pops. Then F2 () does the same, and finally F3 (). After that, the stack is empty and nothing else can execute.

Ok, now let’s look at a more complicated example. This is a function that f3() calls another function, f2(), which in turn calls another function, f1().

function f1() {
  // Some code
}
function f2() {
  f1();
}
function f3() {
  f2();
}
f3();
Copy the code

Let’s see what happens to the function execution stack:

A step-by-step process shows the order of execution

Notice that first F3 () enters the stack and calls another function, f2(). So now F2 () enters and F3 () remains on the stack. The f2() function calls f1(). So, it’s time for f1() to enter the stack, f2() and f3() to remain inside.

First, f1() completes execution and exits the stack. After that, f2() is done, and finally F3 ().

The bottom line is that everything that happens inside the function execution stack is sequential. This is part of SynchronousJavaScript. The main thread of JavaScript makes sure that it processes any content in the stack before it starts looking at anything else.

Great! Now that we know how operations work in synchronousJavaScript, let’s flip a coin and look at its Asynchronous side. Are you ready?

Asynchronous JavaScript — how the browser API and Promise work

Asynchronous in this word means not happening at the same time. What does it mean in the context of JavaScript?

In general, doing things in order works well. But sometimes you might need to fetch data from the server or delay the execution of functions that you didn’t expect from NOW. Therefore, you want code to have asynchronously execution.

In these cases, you probably don’t want the JavaScript engine to stop the execution of other sequential code. So, in this case, the JavaScript engine needs to manage things more efficiently.

We can categorize most asynchronous JavaScript operations using two main triggers:

  1. Browser API/Web APIEvents or functions. These include methods of imagingsetTimeout, or event handlers like clicks, mouse hovers, and scrolls.
  2. Commitment. A unique JavaScript object that allows us to perform asynchronous operations.

If you’re not familiar with Promise, don’t worry. You don’t need to know much more to read this article. At the end of the article, I’ve provided links so you can start learning Promise in the most beginner friendly way possible.

How to deal with browser apis /Web apis

Browser APIsetTimeout and event handlers rely on callback functions. The callback function is executed when the asynchronous operation completes. Here is an example of how the setTimeout function works:

function printMe() {
  console.log('print me');
}

setTimeout(printMe, 2000);
Copy the code

The etTimeout function executes the function after a certain amount of time. In the code above, the text print Me logs in to the console after a delay of 2 seconds.

Now suppose we have a few more lines of code after the setTimeout function, as follows:

function printMe() {
  console.log('print me');
}

function test() {
  console.log('test');
}

setTimeout(printMe, 2000);
test();
Copy the code

So, what do we expect to happen here? What do you think the output will be?

Does the JavaScript engine wait 2 seconds to call the test() function and print:

printMe
test
Copy the code

Or will it manage to keep the setTimeoutaside callback and continue with other execution? So the output might look something like this, maybe:

test
printMe
Copy the code

If you guessed the latter, you’re right. This is where asynchrony comes in.

How JavaScript callback queues work (aka task queues)

JavaScript maintains a queue of callback functions. It is called a callback queue or a task queue. Queue data structures are first-in-first-out (FIFO). Therefore, the first callback that enters the queue has a chance to exit first. But here’s the thing:

  • When does the JavaScript engine put it on the queue?
  • When does the JavaScript engine pull it out of the queue?
  • Where does it go when it comes out of the queue?
  • Most importantly, what does all of this have to do with the asynchronous part of JavaScript?

Wow, so many questions! Let’s find out with the help of the following figure:

The figure above shows the call Stack in the general way we’ve seen it. There are two additional sections that track whether a browser API (such as setTimeout) is started and queue the callback function from that API.

The JavaScript engine is constantly executing functions in the call stack. Since it does not put the callback function directly on the stack, there is no problem with any code waiting/blocking execution on the stack.

The engine creates a loop that periodically looks at the queue to find what needs to be extracted from there. When the stack is empty, it pulls the callback function from the queue onto the call stack. Now the callback function usually executes like any other function in the stack. The cycle continues. This Loop is called an Event Loop.

So, the moral of the story is:

  • Park the callback function in a queue when the browser API occurs.
  • Continue to execute code on the stack as usual.
  • The event loop checks if there are callback functions in the queue.
  • If so, the callback function is pulled onto the stack from the queue and executed.
  • Continue the loop.

Ok, let’s see how it works with the following code:

function f1() {
    console.log('f1');
}

function f2() {
    console.log('f2');
}

function main() {
    console.log('main');
    
    setTimeout(f1, 0);
    
    f2();
}

main();
Copy the code

This code executes a setTimeout function f1() with a callback function. Please note that we have given it zero delay. This means we want the function f1() to execute immediately. After setTimeout, we execute another function, f2().

So, what do you think the output will be? Here is:

main
f2
f1
Copy the code

However, you might think that F1 should print f2 first because we don’t delay f1 execution. But no, that’s not the case. Remember the event loop mechanism we discussed above? Now, let’s look at it in the step-by-step flow of the code above.

Event loop – View step-by-step execution

Here are the written steps:

  1. themain()The function enters the call stack.
  2. It has a console log to print the word main. inconsole.log('main')Execute and enter the stack out.
  3. SetTimeout browser API occurs.
  4. The callback function puts it into the callback queue.
  5. In the stack, execution happens as usual, sof2()Enter the stack.f2()Console logs for execution. Both are out of the stack.
  6. themain()Also pops the stack out.
  7. The event loop recognizes that the call stack is empty and there are callback functions in the queue.
  8. Then the callback functionf1()Enter the stack. Execution begins. Console log execution,f1()It also comes off the stack.
  9. At this point, there is nothing further on the stack and queue to execute.

I hope you now have a clear idea of how the asynchronousJavaScript parts work internally. But that’s not all. We want to look at Promises.

How does the JavaScript engine handle promises

In JavaScript, promises are special objects that help you perform asynchronous operations.

You can use the Promise constructor to create a Promise. You need to pass it an executor function. Within the Executor function, you can define the actions to perform when a Promise successfully returns or throws an error. You can do this by calling the resolve and Reject methods, respectively.

Here’s an example of a promise in JavaScript:

const promise = new Promise((resolve, reject) = >
        resolve('I am a resolved promise'); ;Copy the code

After executing the promise, we can use the.then() method to handle the result and any errors in the method. Catch ().

promise.then(result= > console.log(result))
Copy the code

Promise is used each time the fetch() method is used to fetch some data from the store.

The important point here is that the JavaScript engine does not use the same callback queue as we saw earlier in the browser API. It uses another one called the Job Queue.

What is a job queue in JavaScript?

Each time a promise appears in the code, the executor function enters the job queue. The event loop works as usual to view the queue, but prioritized the Job Queue item over the callback Queue item stack when idle.

The macro Task’s item in the callback queue is called A, and the job queue’s item is called a Micro Task.

So the process goes like this:

  • For each loop, fromevent loopTo complete a taskcallback queue.
  • Once the task is complete, the event loop is accessedjob queue. Itmicro tasksComplete everything in the job queue before looking at the next thing.
  • If both queues get entries at the same point in timejob queuePrior to thecallback queue.

The following figure shows the containing job queue and other pre-existing items.

Now, let’s look at an example to better understand this sequence:

function f1() {
    console.log('f1');
}

function f2() {
    console.log('f2');
}

function main() {
    console.log('main');
    
    setTimeout(f1, 0);
    
    new Promise((resolve, reject) = >
        resolve('I am a promise')
    ).then(resolve= > console.log(resolve))
    
    f2();
}

main();
Copy the code

In the code above, we have a setTimeout() function as before, but we introduce a promise after it. Now remember everything we learned and guess the output.

If your answer matches this, you are correct:

main
f2
I am a promise
f1
Copy the code

Now let’s look at the flow of the action:

Callback queue and job queue

The process is almost the same as above, but it is important to note how items in the job queue take precedence over items in the task queue. Also note that it doesn’t matter if the setTimeout delay is zero. It is always associated with the job queue before the callback queue.

Ok, so we’ve learned everything you need to understand synchronous and asynchronous execution in JavaScript.

Here’s a quiz for you!

Let’s test your understanding with a quiz. Guess the output of the following code and apply everything we’ve learned so far:

function f1() {
 console.log('f1');
}

function f2() { 
    console.log('f2');
}

function f3() { 
    console.log('f3');
}

function main() {
  console.log('main');

  setTimeout(f1, 50);
  setTimeout(f3, 30);

  new Promise((resolve, reject) = >
    resolve('I am a Promise, right after f1 and f3! Really? ')
  ).then(resolve= > console.log(resolve));
    
  new Promise((resolve, reject) = >
    resolve('I am a Promise after Promise! ')
  ).then(resolve= > console.log(resolve));

  f2();
}

main();
Copy the code

This is the expected output:

main
f2
I am a Promise, right after f1 and f3! Really?
I am a Promise after Promise!
f3
f1
Copy the code

Do you want more tests like this? Head to this repository to practice more exercises.

If you have questions or need any instructions, my DM is always open on Twitter.

In a word

To sum up:

  • The JavaScript engine uses stack data structures to keep track of the currently executing functions. This stack is called the function execution stack.
  • The function execution stack (aka call stack) executes functions sequentially, line by line, line by line.
  • The browser /Web API uses the callback function to complete the task when the asynchronous operation/delay completes. The callback function is placed in the callback queue.
  • The promise executor function is placed on the job queue.
  • For each loop of the event loop, one macro task is completed from the callback queue.
  • Once the task completes, the event loop accesses the job queue. It completes all the microtasks in the job queue before looking for the next thing.
  • If both queues get items at the same point in time, the job queue takes precedence over the callback queue.

Before we go…

So far so good. I hope you found this article insightful and helped you better understand the concepts of synchronization and asynchrony in JavaScript.

Let’s connect. You can follow me on Twitter(@tapasadhikary), My Youtube channel and GitHub(Atapas).

As promised, here are some articles you might find useful,

  • JavaScript Promises – Explain like I was five years old
  • JavaScript Promise Chain – The art of handling Promises
  • JavaScript Async and await – please use simple English
  • Introduction to PromiViz – Visualize and learn JavaScript promise apis