As the internet evolved, websites and web applications became more interactive and dynamic, requiring more intensive operations such as retrieving API data through external network requests.
To handle such scenarios, developers must use asynchronous programming techniques. This approach is essential as it allows the browser to process tasks in parallel, enabling users to continue using the browser normally while asynchronous operations are being executed.
Let’s begin this section by understanding the call stack first:
Let’s understand it with one example:
When running the code above, the first starts with the global execution context main() function on the browser, where simultaneously JS engine starts reading the code line by line until it reaches the last line to finally find and execute tasks (function call).
Afterward, the function will be directed to the Call Stack for further task execution.
At first, it will execute the statement “Time to breakfast!” and then move on to the next line, where the asynchronous block of code resides.
Over time, the setTimeout() function awaits inside the Web API.
For this example, Web API takes the callback function: setTimeout(lunch, 3000); turn on the timer and pass this call to the callback queue once the 3 seconds have passed on.
The callback function opts for the Callback queue after the time runs out (in this case, it’s 3 seconds) and waits for the Call stack to get the bandwidth.
By this time, the Call stack jumps on another block of code and prints the statement: It’s time for dinner!”
But when the callback waiting event loop comes as a guardian of this function, ensuring effective communication between Call Stack and Callback Queue.
The Message Queue is a data structure that holds tasks waiting to be executed. And when an asynchronous task is called, it is added to the Task Queu
The Event Loop checks the Call Stack! And if it is empty, it checks the Task Queue for tasks to be executed. If there are tasks in the Task Queue, the Event Loop takes the first task in the queue and pushes it onto the Call Stack, where it is executed.
When the task is completed, any functions that were called during its execution are added to the Call Stack. These functions are then executed, and when the Call Stack is empty again, the Event Loop checks the Task Queue for the next task to be executed.
It is the traditional approach where tasks are executed one at a time, in sequence. In this approach, the program waits for each task to complete before moving on to the next one.
That means when a task is executed, the program is blocked from executing any other code until the task has been completed. It can result in long wait times and unresponsive applications, especially when dealing with time-consuming operations like network requests or file I/O.
It is a modern approach where tasks are executed independently without blocking the main thread of execution. In this approach, the program continues to execute other code while waiting for the task to complete.
When a task has been completed, it triggers a callback function, which will be executed in the background, which allows the program to handle multiple tasks concurrently, making it more efficient and responsive.
In summary, synchronous programming executes tasks in sequence, one at a time, blocking the main thread for each process to complete.
Asynchronous programming executes tasks independently, allowing the program to continue running other code while waiting for the current process to complete. Also, it is essential for creating responsive and efficient web applications that can handle multiple tasks concurrently.
Let’s learn them through an example:
You can illustrate the difference between Synchronous and Asynchronous code very simply:
The second line will be executed when the click event occurs somewhere on the page. Here, the process can be marked as asynchronous because the call for the event loop awaits.
Without a click event, the handle won’t even bother to execute the code and even when the other event is in the process. Learning lessons, when a code runs for a longer period, the event processing will be left awaited, and the website goes into standby mode.
Therefore, asynchronous events come for help!
This block of code runs the XMLHttpRequest – known asXHR and starts with an open() function. The code opts for the callback function to respond. The send()submits the requests while the program is running. And finally, the output will be printed with the function alert().
Nested callbacks occur when a function executes an asynchronous operation and passes a callback function as an argument to handle the result of that operation. If that callback function also performs an asynchronous operation, it may need to pass another callback function to handle its result.
It can lead to a situation where multiple asynchronous operations are nested inside each other, resulting in a callback hell or callback pyramid.
Here’s how it’s done:
In this code, every setTimeout function is nested into the increasing number of the function, which makes the pyramid dense with nested callbacks. When running, you’ll get the output as follows:
In practice, asynchronous code handling can get a little complicated. The reason is, at first, we need to do error handling for the asynchronous code and then send the data for the next request. And when the callback queue emerges, it makes understanding the code structure even harder, Which makes the pyramid of doom. So, what is it?
Let’s understand it with a practical example:
When dealing with this, you need to associate every function with its potential response and error, to avoid callbackHell creating great confusion.
So, when this is executed, the outline you see as follows:
However, handling asynchronous code is much more difficult. So, to avoid nested callbacks and the Pyramid of Doom, developers can use Promises, sync/await, or other asynchronous programming techniques. These techniques provide a more readable and maintainable way to handle asynchronous operations without the need for nested callbacks.
But how is it done? By creating and consuming promises!
A promise stands for an object to successfully run an asynchronous function and does have the potential to output the value in the future. You can even call it a strong alternative to a callback function with some additional features and easy-to-understand syntax.
Let’s get started with creating a promise:
When initializing a promise in a web browser’s console, you’ll find two properties: “pending” and “undefined,” which also will be reflected in an output.
As long as you don’t define the promise and fulfill it, it will remain pending forever. So, there comes your first task to fulfill the promised value:
Now, when inspecting the promise, you’ll get the output as follows:
So, it’s clear now that the promise has three states:
Pending – Initial state before getting rejected or resolved
Fulfilled – A successful resolvent to the promise
Rejected – A failed promise operation
The later part of the code shows the fulfilled/resolved promise status!
Now, let’s take a look at the way to consume the promise we just created:
Promises have a then method, which runs when promises are fulfilled or resolved in the code and returns the Promise value as a parameter.
This is how it is done!
In the promise creation step, the code contained [[PromiseValue]] of a “We did it!” statement. So, when running the above code, it will throw that value as a response:
Until now, we haven’t made use of asynchronous Web API. So, now, we are going to test the asynchronous call request by making the use of setTimeout function as a function:
The then method with a parameter response will get called only after the setTimeout operation is processed as mentioned time limit, which is 2000 ms. So, the output will be called in the following manner:
Now, let’s fulfill the promise value using the code given below:
So, the output will be executed as given below by fulfilling the then log with a value:
As then is chained, it allows developers to consume the promise values in a synchronous and readable way than callbacks with easy-to-maintain and verify methods.
When an asynchronous task is initiated, it is added to the event queue, and its associated event listener waits for the event to occur.
Once the event occurs, the event listener is triggered, and its task is added to the event queue. The event loop processes the queue in the order in which tasks are added, removing each task and executing it.
Synchronous tasks are executed immediately, whereas asynchronous tasks are initiated, and their associated callback functions are added to the event queue.
Avoid Blocking the Main Thread
Use Promises and Manage Async Functions with async/await
Be Careful with Loops and Recursion
Loops and recursion can be powerful tools for writing efficient code, but they can also cause the event loop to become blocked if they are not used carefully. To optimize the event loop, it is essential to use loops and recursion sparingly and to ensure that they do not cause the main thread to become blocked.
Use the Right Data Structures
The choice of data structure can have a significant impact on the performance of the event loop. For example, using a Map or Set instead of an array can provide faster lookups and better performance. Additionally, using data structures that are optimized for the specific task can help reduce the amount of time it takes to execute tasks.
Monitoring the performance of the event loop is crucial for identifying potential issues and optimizing performance. Tools such as the Chrome DevTools performance panel can provide insights into how the event loop is performing and help identify areas where optimization is needed.
Asynchronous programming is crucial for handling time-consuming tasks, and it is achieved through the use of callbacks, promises, and async/awaits. However, nested callbacks and the Pyramid of Doom can create readability and maintainability issues in asynchronous code.
You can visit our news and projects on a weekly basis. Or you can subscribe to email for regular email updates.