Asynchronous JavaScript
Overview
JavaScript is a single-threaded programming language that is very popular in Web Development. JavaScript can be synchronous as well as asynchronous. The browser also performs numerous functions; for example, Various Web APIs are accessed by the browser, such as setTimeout(), fetch(), etc., which helps to gain asynchronicity in JavaScript. The role of Callback functions in Javascript is also very important for achieving asynchronicity. It is also important to know the working of JavaScript and understand its behavior and process of code execution. So without further ado, let us dive deep into the world of asynchronous JavaScript.
Execution of JavaScript Code "Synchronously"
JavaScript is a Synchronous single-threaded programming language, but we can also perform asynchronous tasks in JavaScript. Before understanding asynchronous JavaScript, it is important to understand the synchronous behavior of JavaScript. Let us understand this with an example:
Output:
To understand the above-mentioned example, firstly we need to understand two important terms: Execution Context and Call Stack.
The Execution Context
As the name suggests, here 'Execution' refers to the JavaScript code execution. Actually, 'Execution Context' is a theoretical or conceptual term in JavaScript which refers to the environment where the JavaScript code is (expected to be) executed. This simply means that the context in which the JavaScript code runs or executes is the 'execution context'. You will understand this thing more clearly in the later sections of this article.
The Call Stack
In JavaScript, the language operates on a single-threaded model, meaning that it can only perform one task at a time. This is facilitated by a single call stack within the JavaScript engine, responsible for managing execution contexts such as the Global Execution Context (GEC) and Function Execution Context (FEC). The call stack acts as a record of functions or code to be executed, following the Last In First Out (LIFO) principle. Essentially, the code instruction or statement that is most recently added to the call stack is executed first, and subsequently removed from the stack.
Now is the time to understand the above given example as we have introduced you to the important terms that are going to be used in this explanation, such as Call Stack and Execution Context.
Refer to the below-given Stacking process in the Call Stack.
During code execution, a Global Execution Context (represented by main() in the given code) is created and pushed to the top of the stack. Then, console.log(10) is added to the call stack and immediately executed before being removed from the stack. Following this, bestFunc() is called and added to the top of the call stack. Within bestFunc(), goodFunc() is called and also added to the call stack. The statement console.log("good") within goodFunc() is executed and then removed from the stack. This process continues according to the Last In First Out (LIFO) principle until main() is eventually removed from the call stack. It is important to note that code execution takes place sequentially within the call stack. This synchronous execution pattern in JavaScript can lead to a problem known as "blocking."
Blocking
The code execution in JavaScript happens sequentially in the call stack, which may lead to a phenomenon called ‘Blocking’. If there’s a code or a function that takes some time to perform its tasks, the code or the functions below or after that code have to wait till that specific(time taking) function gets executed; hence when the code after a particular code block is blocked due to some delay in that particular code, the phenomenon is called as ‘Blocking’.
We will discuss this in more detail with examples ahead in this article after understanding the asynchronous JavaScript but before understanding the asynchronous code execution in JavaScript, let's dive into the world of Callbacks in JavaScript.
The Callbacks in JavaScript
Functions are the first-class citizens in JavaScript, which makes them very powerful with various special features. One such special feature is that functions can be passed inside other functions as arguments, and then they(the passed functions) can be used later in those functions(the parent functions). Here, the passed function(as an argument) is called "Callback function". Refer to the image given below.
Output:
In the above-given image, there are two functions: One() and Two(), that is used for explaining the callback functions in JavaScript. As you can see in the image, the function One() is passed as an argument inside the function Two() and then, after some time, it can be called in function Two() whenever required.
In the above-mentioned example, when we called function One(), the statement inside the function One() got executed, and "One is called" got printed in the console. Then we passed the function One() as the callback function inside the function Two(), when we called the function Two(), the first statement inside the function wo() got executed, and "Two is called" got printed in the console. Then the second statement got executed, in that statement, the callback function was called; hence the console.log statement inside the callback function (One()) got executed, and "One is called" got printed in the console. Now we will look at another example of callback functions in JavaScript with respect to the setTimeout().
Output:
Note The setTimeout() method calls the callback function after the specified timer expires. The timer or delay accepts time in milliseconds.
1 second = 1000 milliseconds,
2 seconds = 2000 milliseconds, and so on.
In the above-given example, we have two callback functions: One is an anonymous arrow function inside the first setTimeout() and the other is the normal function inside the second setTimeout().
We have set the timer to 2 seconds (2000 milliseconds) in both of the setTimeout(). When the program executes, "I am from setTimeout" gets printed inside the console two times after the timer of two seconds expires, one from the first setTimeout() and another from the second setTimeout(). So now it is clear what the callback function is, but another arises here, and that is Why is it called "callback function"?
It is because the callback function is called back after some time in the parent function when needed, and then it is executed. It is important to know that the callback functions are very powerful as they provide access to the whole asynchronous world in a Synchronous Single-threaded programming language that has only one call stack in which the code is executed in a specific order. Let us see an example of async callbacks.
In the given example, we used a DOM event listener in JavaScript with addEventListener(). The first parameter specifies the event type, and the second parameter is an async callback function executed after the event is fired. We added a listener to a button in HTML, using 'click' event and an anonymous arrow function as the callback. When the user clicks the button, an alert appears with a message, and upon clicking 'ok', an h4 element is added to the document. The callback function passed to addEventListener() is executed asynchronously. When passing a callback function, only its reference is passed as an argument. We don't use round brackets to execute the callback immediately. The callback is called back asynchronously in the parent function's body. The parent function takes responsibility for executing the callback at the appropriate time.
How Does Asynchronous JavaScript Work?
Asynchronous as the name suggests means 'not' synchronously. The call stack inside the JavaScript engine can execute only one code at a time and if we have some function like the one which fetches the data from the servers then it can take some time, due to which, the other code below or after that function has to wait for that code(time-consuming function) to get completed and executed. This is a problem of time hence can be easily solved using a timer or delay. But wait, there is no such thing inside the call stack called "timer". So here comes a superhero into the picture which is nothing but 'Browser'. So let us look at the involvement and power of Browser.
The Browser
Someone said that don't underestimate the power of a common man and a browser it is really very powerful. We already know that the code is executed inside the call stack, this call stack is located inside the JavaScript Engine which is located inside the Browser.
The browser has access to various things like local storage access, timer access, geo-location access, Bluetooth access, URL, etc. with the help of Web APIs. This means that the browser can communicate with the external world. So, We can say that it is the browser that combines the power of Web APIs with the JavaScript Call Stack and fulfills the need of the call stack such as providing the delay(timer) for some callbacks with the help of setTimeout() method. So the Browser, with the help of Web APIs makes it possible for the code inside the call stack to have a timer(and other such facilities) available.
The Web APIs
There are various Web APIs like setTimeout(), fetch(), console(o yeah it is also one of the Web APIs), etc that are used by the browser to fulfill the need of the call stack. Now it is important to know that the setTimeout(), console, and all these Web APIs are not part of JavaScript. Yes, you read that right. These are part of Web APIs and are connected or combined with the call stack of JavaScript Engine with the help of Browser. We can also say that Browser acts as the communicating medium between the call stack and the Web APIs. Let us see these Web APIs inside the code practically with the example of setTimeout(). We will also look into the working of setTimeout() and event listeners. While doing so, we will encounter another phenomenon called "Blocking".
What is Blocking?
As we have discussed above, the code execution in JavaScript happens sequentially in the call stack, which may lead to a phenomenon called 'Blocking'. If there's a code or a function that takes some time to perform its tasks, the code or the functions below or after that code have to wait till that specific(time taking) function gets executed; hence when the code after a particular code block is blocked due to some delay in that particular code, the phenomenon is called as 'Blocking'.
Let us understand with an example.
In the above example, due to the synchronous behavior of JavaScript, someFunction() will only get executed when the fetchData() finishes execution. But fetchData() is performing some network requests, which can take a considerable amount of time. As fetchData() is going to take some time to fetch the data from some server(just assume it) the code or function after the fetchData() has to wait for it to complete the execution. It means that the code after fetchData() is blocked, and this phenomenon is referred to as "Blocking". This results in unnecessary time consumption, which can create a negative impact when performing huge operations. Hence it is necessary to solve this problem. This can be solved using the callbacks.
For example:
Note: It should be noted that the web APIs, the event loop, the task queue, and the microtask queue are not part of the JavaScript engine, instead, they are a part of the browser’s JavaScript runtime environment. While in Nodejs, the web APIs are replaced by the C/C++ APIs.
In the above-given example, you can see that we have used the setTimeout() method, which is a part of Web API, the code is executed asynchronously which eventually results in the unblocking of code.
During the code execution, a call is made to fetchData(), so it’s pushed to the top of the call stack, then the setTimeout() function(which is inside the fetchData() function) is called and it’s pushed to the top of the stack. The setTimeout() method has two arguments: A callback (function) and delay (in milliseconds). Now the setTimeout() method starts a timer of 3s in the web APIs environment. After this, the setTimeout() is popped out from the stack. Then someFunction() is pushed to the stack, executed, and then removed from the stack after its completion. During this process, the timer(3-second timer) that was started by the setTimeout() method expires, due to which the callback function is pushed in the message queue. But here's a twist, the execution of the callback is not so quick and that's because of the event loop.
But what is it? Let's have a look.
The Event Loop and the Callback Queue in JavaScript
So here comes a very interesting part of our article: the event loop.
Let us understand this by an example.
Output:
During code execution, a GEC is created, and the first code is executed immediately, printing "I am start" in the console. The subsequent code is pushed to the call stack and executed, logging "I am end". Meanwhile, a timer starts with a duration of 4 seconds. After the timer expires, the callback registered in the Web APIs environment is ready for execution. The callback is sent to the callback queue, and the event loop checks if the call stack is empty. If it is, the callback is pushed to the call stack and executed. The Event Loop's main task is to ensure the callback enters the call stack when it's empty. The callback queue exists due to the presence of multiple callbacks in a program. It ensures that callbacks are executed in the sequence they occurred in the queue. There is also a microtask queue for promises, where callbacks have priority over the task queue's callbacks. Note: The microtask queue prioritizes the execution of callbacks over the task queue.
The Promises in JavaScript
Until now, we have seen a technique to handle asynchronous JavaScript, which was called 'Callbacks'. Now we are going to see another technique to handle asynchronous JavaScript called "Promises". As we have promised in the real world, similarly, we have promises in JavaScript. When we promise someone, that promise is either fulfilled by us or not fulfilled by us. In the same way, promises in JavaScript can either be fulfilled (resolved) or not fulfilled(rejected).
Working of Promises
A Promise is an Object which has three states: Pending state, Resolved state, and Rejected state. As the words suggest, when the promise is not made yet, the state is called a Pending state. When the promise is successfully fulfilled, the state is called Resolved state, and when the promise fails to get fulfilled, the state is called Rejected state.
Refer to the image given below.
In the above diagram, you can see that the first state of the promise is the Pending state, then if the promise is resolved, .then(anyFunction) is called for asynchronous operations, and if the promise is rejected, the .catch(anyFunction) is called for exception(error) handling. The promise again goes into the pending state if there are multiple promises and the process of .then() and .catch()continues till the last promise.
We can use the Promise() constructor to create a new Promise object.
Example of Promises
Now we are going to see an example of promises which will eventually crystal clear your concept of Promises.
In the above-mentioned code, we have a promise called 'promIse' which accepts a function. we have passed an anonymous arrow function with two parameters: resolve and reject. Let us assume that there is some data that needs to be fetched from the server.
Now for simplicity, we assume that if a certain condition is met, then resolve the promise and if it does not meet, then reject the promise. We have set the value of the variable 'anyCondition' to be false, as the condition is not met due to being false, the else block gets executed, and the promise gets rejected hence the .catch() method is executed. We have console logged the message in the .catch() that we received from the reject in the promise. Hence 'Promise is rejected' gets printed in the console. Similarly, if the condition is set to be true, the promise will be resolved, and hence .then() method will get executed, and 'Promise is resolved' will be printed in the console.
If you notice the code, we have attached the .then() method to the promise and .catch() method to the .then() method. This direct attaching of methods is called 'Chaining'. Chaining, as the name suggests, simply means to chain the callbacks. Hence, In Promises, the callbacks are chained, which eventually makes the code more readable and easier for debugging.
Promises Versus Callbacks in JavaScript
Promises | Callbacks |
---|---|
In Promises, the callbacks are chained, which makes the code more readable | Passing Callbacks directly leads to a state called 'callback hell', which makes the code difficult to read |
Promise callbacks are called in the strict order of occurrence in the microtask queue | Callbacks are not always called in the strict order of occurrence in the task queue |
Promise callbacks are given priority while execution as they are stored in the microtask queue | Callbacks are not given priority while execution as they are stored in the task queue |
Error handling is really good with Promises | Error handling is not that good with Callbacks |
Promises do not lose the control of how the function will be executed when passing a callback to a third-party library | Callbacks lose the control of how the function will be executed when passing a callback to a third-party library |
FAQs
Q: What is asynchronous JavaScript and how does it work?
A: Asynchronous JavaScript refers to the ability of JavaScript code to execute tasks non-sequentially, allowing other code to run simultaneously. It works by utilizing features like callbacks and Web APIs provided by the browser to handle time-consuming operations without blocking the execution of other code.
Q: What are callbacks in JavaScript and how are they used in asynchronous programming?
A: Callbacks are functions that are passed as arguments to other functions and are executed at a later time or when a specific event occurs. In asynchronous programming, callbacks are commonly used to handle the results of asynchronous tasks, such as network requests or timer-based operations, ensuring that the code executes in the desired order.
Q: How does JavaScript handle blocking and why is it important in asynchronous programming?
A: Blocking refers to the phenomenon where code execution is delayed due to a time-consuming task, causing subsequent code to wait. In asynchronous programming, it is important to avoid blocking to ensure that other tasks can be executed concurrently. By using asynchronous techniques like callbacks, JavaScript can perform non-blocking operations, allowing the execution of other code while waiting for tasks to complete.
Q: What is the role of the browser in handling asynchronous JavaScript?
A: The browser plays a crucial role in handling asynchronous JavaScript by providing Web APIs that enable non-blocking operations. These Web APIs, such as setTimeout() and fetch(), allow JavaScript code to interact with external resources and set timers. The browser combines the power of Web APIs with the JavaScript call stack to ensure the asynchronous execution of code.
Q: Are callbacks the only way to achieve asynchronous behavior in JavaScript?
A: No, callbacks are not the only way to achieve asynchronous behavior in JavaScript. While they are a common approach, other techniques like promises and async/await are also used. Promises provide a more structured and better way to handle asynchronous tasks, while async/await syntax simplifies the use of promises, making asynchronous code appear more synchronous and easier to read.
Conclusion
- JavaScript is a single-threaded programming language.
- JavaScript has only one call stack, which is used for code execution.
- Blocking is very dangerous and affects the user experience badly.
- Blocking can be avoided or solved using asynchronous JavaScript.
- Browsers can communicate with the outside world with the help of various Web APIs.
- Browsers combine the power of Web APIs with the call stack.
- setTimeout() is not a part of JavaScript, instead, it is a Web API.
- Console and fetch() are also part of Web APIs.
- Functions that are passed inside other functions as arguments are called Callbacks in JavaScript.
- Callbacks introduce us to the whole world of asynchronous JavaScript.
- The main task of the event loop is to check the emptiness of the call stack, and if it is found to be empty, it allows the callback to enter the call stack.
- Promises are stored in the microtask queue and are given priority over normal callbacks by the event loop.
- Callback hell can be avoided by using Promises.