Asynchronous programming is a technique that allows you to execute a resource-intensive task in a non-blocking way. This means that while the task is being executed, the main thread of execution can continue to run other tasks. Asynchronous operations are commonly used in JavaScript for tasks such as making network requests, Fetching files, and querying databases.
Let’s revisit the longtask
function and modify it to use an asynchronous operation:
In the code above, we replaced the for
loop in the longtask
function with a setTimeout
function. The setTimeout
function that allows you to schedule a function to run after a specified delay (in milliseconds). In this case, we are scheduling a function to run after 5000 milliseconds (5 seconds).
When you run this code, you will see that the longtask
function no longer blocks the execution of the subsequent tasks. The output will be:
Aside: The setTimeout
function is a browser API that is not part of the JavaScript language itself. It is provided by the browser environment and allows you to schedule code to run after a specified delay. In Node.js, you can use the setTimeout
function as well.
Restaurant Metaphor: A synchronous blocking restaurant is where the waiter will wait until your food is ready and served before helping another customer. An asynchronous non-blocking restaurant is where the waiter takes your order, gives it to the chef, and then moves on to take another customer’s order while the chef prepares your food. When your food is ready, the waiter serves it to you.
House Cleaning Metaphor: In a synchronous blocking system, you wash the dishes, and when you are done, you take the garbage out. In a non-blocking asynchronous system, you put the dishes in the dishwasher and start it. While the dishes are being washed, you clean the kitchen and take the garbage out. Once the dishes are done, the dishwasher will beep to call on you so you can put the clean dishes in the cupboards.
Some programming languages allow you to create multiple threads of execution, known as multi-threading. In multi-threading, each task runs on a separate thread, allowing multiple tasks to run concurrently. This can be useful for CPU-bound tasks that require a lot of processing power. However, managing multiple threads can be complex and error-prone.
Restaurant Metaphor: A multi-threaded restaurant is like having multiple waiters, each serving a different customer. Each waiter can take orders, serve food, and handle payments independently. Each server will wait for the food to be ready before serving it to the customer. However, since there are multiple waiters, they can serve multiple customers simultaneously.
JavaScript, on the other hand, is a single-threaded language, meaning that it has only one thread of execution. This single thread is responsible for running your JavaScript code, handling events, and updating the UI. While this may seem limiting, JavaScript leverages asynchronous programming to perform resource-intensive tasks without blocking the main thread.
So how does JavaScript behave asynchronously? The JavaScript engine relies on the hosting environment, such as the browser, to tell it what to execute next. Therefore, any async operation is taken out of the call stack and managed by the hosting environment. (This could be done concurrently). Once the async job is completed, it will be placed in a callback queue to return to the call stack. This cycle is monitored by a process called the event loop.
The Event Loop monitors the Call Stack and the Callback Queue. If the Call Stack is empty, it will take the first event from the queue and push it to the Call Stack, which effectively runs it. This process is repeated until the Call Stack is empty.
Let’s explore this process for our earlier example:
When you run this code, this will happen under the hood:
task(1)
is added to the Call Stack and executed.task(1)
invokes console.log("Task 1")
and the console.log
function placed in the Call Stack and executed.console.log
function is done printing “Task 1” to the console, it is removed from the Call Stack.task(1)
, so it is removed from the Call Stack.longtask(2)
is added to the Call Stack and executed.longtask(2)
invokes console.log("Task 2 started!")
and the console.log
function placed in the Call Stack and executed.console.log
function is done printing “Task 2 started!” to the console, it is removed from the Call Stack.longtask(2)
invokes setTimeout(() => console.log("Task 2 finished!"), 5000)
and the setTimeout
function placed in the Call Stack and executed.setTimeout
function schedules the callback function to run after 5000 milliseconds. This is handled by the browser environment (or Node.js environment).setTimeout
function is removed from the Call Stack.longtask(2)
, so it is removed from the Call Stack.task(3)
is added to the Call Stack and executed.task(3)
invokes console.log("Task 3")
and the console.log
function placed in the Call Stack and executed.console.log
function is done printing “Task 3” to the console, it is removed from the Call Stack.task(3)
, so it is removed from the Call Stack.task(4)
is added to the Call Stack and executed.task(4)
invokes console.log("Task 4")
and the console.log
function placed in the Call Stack and executed.console.log
function is done printing “Task 4” to the console, it is removed from the Call Stack.task(4)
, so it is removed from the Call Stack.setTimeout
is added to the Callback Queue.() => console.log("Task 2 finished!")
is executed.console.log
function is placed in the Call Stack and executed.console.log
function is done printing “Task 2 finished!” to the console, it is removed from the Call Stack.There are three patterns for handling asynchronous operations in JavaScript:
We will look at each in the following sections.