Akshay's Blog
Aug 11, 2022

Why just using async-await and Promises doesn’t make your code asynchronous

Many developers are under the misconception that if they add “

async

” before their function definition, return a promise and use a “

.then”

or “

await

” on that promise, their code will be non-blocking (asynchronous). While awaiting the resolution of that promise, other code, such as handling UI interaction events is free to run.

This article busts this myth with a few examples and tells you when your code is truly asynchronous and non-blocking.

. . .

JavaScript is single-threaded. It only uses

one thread to run all your code

. Let us look at an example of some asynchronous code:

Take a moment and try to figure out what this code is doing. We are calling the async function

OrderIceCream()

which will create a timer for 7 seconds and execute a callback afterwards that will log “Your ice cream has arrived” and resolve the promise with the message “Ice cream delivered”, which will be logged by the “

.then

” callback. We are also calling the

EatPizza() 

function. We want to order some ice cream and eat pizza while it is on its way, after which we will accept the delivery.

What do you think will be the order of the

console.logs

? Just copy the above code and paste it into the browser console (f12) or run it using Node.

1_72I_FO66FJ9tpOPTvbX21w.png

As we can see, “Ordering ice cream” was logged first followed by “Eating pizza”. 7 seconds later, “Your ice cream has arrived” was logged followed by “Ice cream delivered”. This is just what we wanted!

While the timer was running, the JavaScript main thread was not blocked. It was free to execute other code (the

EatPizza() 

function). This was an example of asynchronous code.

. . .

Now let’s look at another example, this time we will change 

OrderIceCream()

a little bit:

Now we have removed the

setTimeout()

and instead added a for loop that will run for 10,00,00,00,000 iterations (a very long loop that will take many seconds to complete).

What do you think the order of console logs will be this time? If just returning a promise and scheduling a callback after that promise’s resolution using “

.then()

” (or using async-await) was sufficient to make our code asynchronous and not block the main thread, then we should see the same results as before.

1_ZMC2K97Gk7qxIIQAe5fMRg.png

The actual logging order is contrary to what we had expected. There is a gap of a few seconds between the logs “Ordering ice cream” and “Your ice cream has arrived”, and we do not want to wait for the ice cream to arrive to eat our pizza. Why is this so?

Once again, remember that JavaScript is single-threaded. It uses 1 thread to run all your code. This means that the for loop will also run on the main thread and the rest of the code will only run once this for loop has been completed.

Therefore, if you have synchronous code, even if your function itself is async, it will run on the main thread. Note that the

console.logs

are also synchronous code, but they do not require much time for execution, so they will only block our main thread for a few milliseconds. During this time, if there is a button click event on your UI, the handler for that event will not be executed until whatever the thread is currently executing is finished. For a simple statement such as a

console.log

, this delay is just a few milliseconds, barely noticeable by the user, but for something like the big for loop that runs for 1e10 iterations, the UI will be frozen for many seconds and the user will notice it.

No other code will run until the thread is free

.

I used the example of the big for loop to simulate long-running synchronous code, such as processing a large dataset or converting a long video from one format to another.

So, what can you do so that this long-running synchronous code does not block the main thread? You could use “worker threads” to run this. Although JavaScript itself is single-threaded, the browser that we use to run the JavaScript code has “web worker” threads. Similarly, NodeJs also has “Node worker” threads. You can run the big for loop (or any other long-running code) on a worker thread, this will block the worker thread but leave your main thread free to do other things, such as handle UI events.

. . .

Ok, so even if your method uses promises and “

.then

” or “

async-await

”, the code inside that method itself is synchronous and will execute on the single JavaScript thread, and if that code is long-running, the main thread won’t be able to run anything else for a long time.

But what about

setTimeout() 

in the first example? Where does the timer count the time? Doesn’t it also block the main thread while it is executing? Or do we use worker threads for it so that our main thread is free? And what about awaiting a network response:

In this example, I created a node server that listens on localhost:5000 and returns a response (with the text “Ice cream delivered” as the body) after waiting 5 seconds whenever a request is received. So, what do you think will be the order of the console logs?

Now that we know all code is executed on the same thread, we might think that awaiting the network response will also happen on that thread. This means that we should first see “Ordering ice cream” getting logged, and then the waiting for the response will be done, and after 5 seconds, we will see the response (“Ice cream delivered”) getting logged followed by “Eating pizza”.

1_ajmhKkMhq4FVkyWQ7k3FBA.png

But just like the first example of

setTimeout()

, we see that in this case as well, the

EatPizza() 

function is able to execute while waiting for the response. “Ordering ice cream” is logged first, followed by “Eating pizza” and 5 seconds later, the response from the server (“Ice cream delivered”) is logged.

Why is it so that the timer and awaiting a network response don’t block the main thread? Do they block a worker thread?

To understand how this works, you must first know that timers and network operations are Input-Output Operations. Generally, anything not happening on the CPU is called I/O.

Sending a network request (writing the request to a socket) or reading a response (from a socket) are I/O operations (just like mouse clicks, key presses, or hard disk reads/writes). The device that performs these I/O operations is the network interface card. Counting the time uses the system clock.

Library functions such as

setTimeout()

and

fetch() 

run synchronously on the main thread (blocking it for some time), but the actual work of counting the time, writing to the network interface card and reading the response from the network card are I/O operations.

. . .

There are 2 types of thread blocking:

CPU bound blocking

: The thread is blocked because it is actively executing on a CPU. The second example (1e10 iterations for-loop) above was CPU-bound blocking.

I/O bound blocking

: Here, a thread is blocked waiting for an I/O event to happen. In this case, the thread is not actively executing on a CPU and is in a sleeping state (in the waiting queue in the RAM).

JavaScript does not do I/O bound blocking, it does not block a thread while waiting for I/O events. It implements something called

“asynchronous non-blocking I/O” 

that leaves the thread free to do other things instead of waiting for an I/O event. This is why waiting for network responses or timers to complete does not block a thread. Yes, not even worker threads!

. . .

To summarize:

Just using Promises and “

.then

” or “

async-await

” will not make your code asynchronous (non-blocking). All your code runs on a single thread.

There are 2 types of thread blocking:

CPU bound blocking

: If you have long-running synchronous code in your async methods, it will block the main thread. To overcome this problem, you could use worker threads to run the long synchronous code, which will leave your main thread free to handle other events.

I/O bound blocking

: The thread is blocked while waiting for an I/O event such as a mouse click or a network request/response. To overcome this, JavaScript uses asynchronous programming (async-await, Promises and the event loop) to implement “

asynchronous non-blocking I/O

”. To understand how that works, check out my article:

Ultimately, an async method will truly be asynchronous (non-blocking) only if it calls some other async method (which in turn should call another async method and so on…). At the bottom-most level should be an I/O operation (I/O operations are purely async) or a synchronous method that runs on another thread (it will block the other thread).

Thank you for reading 😃

Akshay Dagar

Akshay Dagar

Software Developer @Microsoft. Writer @BetterProgramming. Programming for more than 7 years. Learning and writing about new technology every day. Love to read. Check out https://akshaydagar.netlify.app.

Leave a Reply