Async != Non-blocking. A Huge Misunderstanding of Asynchronicity

90% of JavaScript developers misunderstand this thing about asynchronicity. (I myself only understood it this past year 😅)

Take a look at the simple code below:

console.log("Before async function");
loopMillionTimes().then(()=>console.log("Returned Promise"));
console.log("After async function");

async function loopMillionTimes(){
 for(let i = 0; i <= 1000_000; i++){
 // something simple
 }
 console.log("Finished Looping");
}

loopMillionTimes function is an async function so it’s returning a promise.

So what is the output that you expect running the code above?? . . . You might initially expect it to be:

Before async function
After async function
Finished Looping
Returned Promise

But if you actually run it, you will be surprised to see that it’s:

Before async function
Finished Looping
After async function
Returned Promise

“But why?! loopMillionTimes is returning a Promise, so it shouldn’t block the main thread!”

Well…

That’s because for a code to be asynchronous, it has to be using an asynchronous API. Returning a Promise doesn’t magically turns your synchronous code into asynchronous one. Nor does it magically make JavaScript multi-threaded.

JavaScript has a single thread. So for it to do 2 things together, it’s asking some EXTERNAL API to do something, & while it waits for the result, it continues executing & do something else with its main thread.

Examples of external APIs??

  • setTimeout & setInterval.
  • fetch.
  • Web Storage
  • Web Sockets
  • Reading files on disk.

These features were never a part of the core JavaScript language itself, they are provided by some external runtime like the browser AND they are being handled by the browser.

JavaScript only invokes a command from this API & gets the result back.

So in our example above, even though the function is async & returning a promise, it’s not using any external API, all of its code needs to be run by Javascript main thread, hence why it is blocking. 🤯

“Okay, but how then can we do syncrounuce long tasks like calculating a large prime number or looping a million times without blocking the main thread for one minute??” Read on!

The Solution

There are 2 ways to make long blocking (synchronous) tasks run in a non-blocking manner & not freeze your whole app. One of them is relatively known, the other one not so much.

The first and commonly known solution is using Web Workers.

Web Workers have been created primarily for solving this problem, they basically allow you to write some code that runs on a separate thread, & can communicate back & forth with the main thread. Resources on Web Workers are already a lot, so no need to repeat that here.

While Web Workers are great, they obviously (as any other great thing) have some limitations. Mainly:

  • No access to the DOM
  • Limited access to Web APIs
  • Data passed between the main thread and workers is copied, not shared. Objects are serialized when they are passed to a worker and subsequently de-serialized on the other end. (& this process is blocking in itself 😅)
  • You can’t pass functions callbacks to workers. (There are some work arounds, but it’s limited)

So depending on your use case, these limitations can prevent you from using Web Workers, & force you to do this long task on the main thread. Which brings us to the 2nd solution.

Let’s think about this for a bit. The problem we have is that once the main thread starts working on this long task, it doesn’t give a chance to any other task to run until it’s done with this task.

But what if each once in a while we can “pause” the task, & give the main thread a chance to process other pending tasks waiting in the macro-task queue! This will ensure that the page remains responsive to user actions & not freeze while this long task is running.

“How can we do that?” We create a function & name it something like yieldToMain (yield ≈ give back). & it usually looks something like this:

const yieldToMain = () => {
 return new Promise(res=>setTimeout(res,0))
}

Looks a bit weird, I know.

But what it’s doing basically is creating a new task that will be added to the macro task queue (immediately), but if you know how the event loop works, it won’t process this task until it process all the other pending tasks in the micro & macro task queues.

So now inside our function that will run for too long, we can call this function each once in a while to pause.

Now what does “each once in a while” mean exactly?? It depends. It could mean you place these pauses manually. If you have a loop, you can place it so that it’s called each X iteration. You can also run it each X interval of time. It's up to you.

Yeah, & that's it!

Hope you learned something new today, & if you found this article helpful, let me know in the comments & share it with your friends.

Until next time,
Have a nice day. 👋

My Photo

About Me

I'm a freelance web developer who specializes in frontend.
I have a special love for React. And my personal goal is: Building things that are Awesome! ✨
If you are someone who is building a project where high quality is a MUST, then Let's Chat! I'll be glad to help you with it.

© 2023-present Mohammed Taher Ghazal. All Rights Reserved.