Parallel Vs Async
Before we get into asynchronous javascript, let's understand the difference between parallel and asynchronous on the grounds of Javascript, as many people confuse these two topics and think they are the same thing.
Parallel
Let's explore the concept of parallelism with a hypothetical scenario involving a simple browser operation on a web page. We will assume a system or laptop equipped with a 6-core processor, running any operating system. The operation involves a fetch call in JavaScript that utilizes XHR (XMLHttpRequest) to retrieve JSON data.
Upon initiating the program execution, the operating system (OS) divides the program into multiple sub-tasks and allocates different threads to these sub-tasks, which work together to execute the primary task. In our illustration, let us assume that the OS divides the program into 3 threads. With 6 cores available, each individual thread is assigned to run on a separate core, enabling the program to simultaneously utilize 3 cores during any given time instance. Consequently, there are still 3 CPU cores unoccupied.
Now, let us consider a scenario where another task emerges, and let's again assume, the OS assigns 3 threads to handle it. These threads are then distributed across the remaining 3 cores. As a result, two tasks run in parallel, with 6 threads executing on different cores, effectively enabling the simultaneous execution of the two distinct tasks at the same time instance. This process, wherein multiple threads or tasks are executed on a multi-core processor, at the same time instance is commonly referred to as parallelism.
Note: In reality, the number of threads created by the OS depends on various factors. and also, there can be more threads than the number of cores in which the OS manages their execution through time sharing or other scheduling techniques.
But as we know that javascript runs entirely on a single thread, So we can safely assume that parallelism is not possible in javascript code execution. now let's move on to understand the concept of asynchronicity Which is actually used in Javascript.
Async
In the context of Asynchronous programming, multiple tasks are executed concurrently in a given time frame, This means that a program can execute more than one task in the given time but only one task is actively executed at any given moment within that frame. This execution pattern allows for task-switching when a task is waiting for a response, enabling the program to proceed with other tasks while waiting for the responses., now there are many nuances on how javascript handles this process of transitioning between tasks and resuming them when responses are received which we will explore as we make progress in the blog.
Need for Asynchronicity
When we encounter time-consuming or blocking tasks that involve waiting for external resources. Our javascript thread will be stuck there until the task is done, this is bad news as I have explained in the previous blog, this blocking behavior in synchronous code execution is problematic. It causes the main thread to be held up, resulting in subsequent operations or tasks having to wait, potentially leading to delays or freezing in the overall application. In such cases, we move to the asynchronous approach which will continue executing our code without blocking as discussed above.
Demystifying Asynchronous JavaScript
Asynchronous programming plays a crucial role in JavaScript when dealing with time-consuming or non-blocking tasks. Unlike synchronous operations that block the execution of code until a task is complete, asynchronous operations allow the code to continue executing while the task is being handled in the background. This enables a more responsive and efficient application, especially when performing operations like fetching data from an API, making network requests, or handling time delays.
"It’s Time to Make Amends and Clear The Air". Following this, we will be discussing the possible solutions, introduced over time to overcome the disadvantages of synchronous code execution, before we move forward, I assume you have a full understanding of how a call stack actually works and how synchronous javascript executes which is clearly explained in my previous blog.
As we know at the very base of our call stack sits a global execution context in which all our global code is stored and is executed line by line through our single thread of execution, when this thread encounters a function call it creates a new execution context for the function and pushes it on top of the call stack and pops it when the function execution is over, so in short this is how our javascript worked till now.
Web API
To achieve asynchronous behavior, JavaScript leverages web APIs such as 'fetch' and 'setTimeout' provided by the javascript runtime environments like browser or nodeJS. These web APIs are designed to handle asynchronous tasks outside the JavaScript runtime. By assigning these tasks to the web APIs, we can free up the JavaScript thread to continue executing the remaining code without waiting for the tasks to complete, unlike the synchronous fashion which blocks the thread. So we can say that "The web APIs handle the execution of asynchronous code for us, enabling asynchronous JavaScript."
However, it's important to note that web APIs are not part of the JavaScript runtime itself rather, they are part of the JavaScript environment provided by the browser. Due to architectural reasons like security and the asynchronous nature of web APIs, they do not have direct access to the call stack. Browsers create a sandbox environment that restricts the access of web APIs to the call stack.
As a consequence, after the web APIs handle the asynchronous tasks, there is currently no direct way to bring the results back into the JavaScript runtime (i.e., into the call stack) for utilization within our javascript application.
JavaScript solves this problem in the form of callbacks and promises. Web APIs return a callback or a resolved promise data after an asynchronous task completes to handle the task's result, depending on the approach chosen by the developer, the callback or promise data is either placed in the "callback queue," which primarily stores callbacks, or the "promise queue," which primarily stores promises. The event loop is responsible for transferring these completed asynchronous tasks from the queues into the call stack, following specific rules.
The terms used in the previous paragraph may seem unfamiliar or confusing, but we will discuss all this technical jargon in detail going further in our blog.
In summary, The web API handles the asynchronous task outside the JavaScript runtime and schedule them for future execution.
Event Loop
Event Loop is a fundamental part of javascript runtime, it holds a very important role in bridging between javascript runtime and its outer environment, Its primary task is to maintain concurrency among all the asynchronous
How the Event Loop Works:
Asynchronous Tasks and Web APIs: After web APIs complete asynchronous tasks, they place these tasks in their respective queues. Common examples of web APIs include setTimeout, fetch, and various DOM events. These queues can be the callback queue and the microtask queue.
Event Loop Execution: The event loop takes charge of bringing these tasks into the JavaScript runtime environment (e.g., browser or Node.js) to resume their execution. It accomplishes this by following a continuous loop that checks for pending tasks.
Call Stack Execution: The event loop retrieves tasks from the queues, prioritizing microtasks over regular tasks which ensure they are processed before the task queue which handles tasks like callbacks, IO events. The event loop then pushes these tasks within the JavaScript runtime's call stack.
In essence, The event loop is a crucial component for handling asynchronous operations, ensuring tasks are processed and executed in a non-blocking manner in the JavaScript environment.
JavaScript's Asynchronous Solutions: Callbacks, Promises & Async/Await
Callbacks vs. Promises
Callbacks were the earliest way to handle asynchronous code. However, they suffer from nested structure readability issues and the problem of inversion of control, making them prone to "callback hell." Promises emerged as a better alternative, offering a chainable structure that improves code readability. Chaining multiple promises calls is possible, but it can still lead to less readable code.
The Rise of async/await
As we have discussed in the course of this blog about how callbacks and promises help in asynchronous code execution, the issue which may arise here is what if our code has both callbacks and promises? and let's imagine a situation where both are completed and waiting in their respective queues to be placed back into the call stack at the same instance, in such case what will happen? whom will the event loop choose?
In such a situation The event loop will always give priority to the promise queue, In fact, if any other promises are waiting in the promise queue, the event loop will handle all the promises and when the promise queue is empty then only the event loop will move to callback queue, as seen in the above figure.
This leads to a specific problem called promise hell/callback starvation. It is very important to keep in mind such cases and try to avoid them, this is where async / await can aid us.
To overcome the challenges posed by callbacks and promises, JavaScript introduced the async/await feature. It offers a more elegant and readable way to manage asynchronous code by allowing developers to write it in a synchronous style. With async/await, complex and nested callback structures can be avoided, resulting in cleaner and more maintainable code. This does not provide a solution to the existing problem as it is just a syntactic sugar coating, but it will help us write and reason about our asynchronous code in a much better way which will help us minimize issues like promise hell and callback starvation.
Comparing Promises and Async/Await
While promises provide a significant improvement over callbacks, async/await takes it a step further. Async/await simplifies code even more by eliminating the need for chaining, leading to a more natural flow. This makes async/await the preferred choice for managing asynchronous operations in modern JavaScript applications.
Finally, we have learned that callbacks, promises and async/await help us not only to execute asynchronous code but there are differences between them and they are improvements over each other solving specific issues like callbacks solve problems from promises and async/await solves problems from promises, as of now async/await are latest and newest feature from javascript which executes our asynchronous code. But there are many things to learn and understand about each method and what are their disadvantages and what are the improvements of each proposed solution. Which we will be discussing in the upcoming Blogs, Until our next post... 👋