loading...
Cover image for A Sneak Peek At Asynchronous JavaScript

A Sneak Peek At Asynchronous JavaScript

srsajjad profile image SR Sajjad Originally published at Medium on ・6 min read

The word "Asynchronous" means  -  something will happen in the future and you are not sitting numb, waiting for that to be finished. It's like you texted someone and the reply could come at any moment and in the meantime you are doing other stuffs.

In JavaScript sometimes we don’t know when one or multiple tasks are going to be finished, all we can do is skip that and check later, again and again. There’s a thing called Event Loop for this.

In this post we’ll look at some basic understating of this async model and common use cases while writing code.

So JavaScript is mainly single threaded.

Every operation gets executed one by one in this main thread.

A. do this
B. do this
C. do this

A will be executed
then B
then C

Serially. Common Sense, right ?
But sometimes, it’s not the case. Let’s see -

let name = "Heisenberg"

This variable name has a value. You want to print out this value.

console.log(name)

What if this value/data isn’t available in your code. It’s somewhere else outside . Maybe some server serves this value when we send HTTP request.

Or maybe it’s inside a file.

So it’s not in your code right now. Your program will have to fetch it from outside.

Now the code looks like this -

let name;

// some imaginary Service
// which sends us a String value as response

fetch("/saymyname")
  .then( res => res.text() )
  .then( value => name = value )

console.log(name)

There’s a bug in the code.

The output would be -

undefined

name variable is still undefined as it was initialized. It wasn’t overridden as we wanted to do inside the fetch code.

That’s because JavaScript skips this fetching operation and continues executing the following lines of your code.

This fetching happens in the background by the Operating System and we get a Promise in our code, that when the resolved value will be available, we can use that data. Just like we are using the value inside then. And that’s why we’ll have to move our printing operation there too.

let name

fetch("/saymyname")
  .then( res => res.text() )
  .then( value => {
     name = value
     console.log(name)
   })

We've just used some Async code.

Normally JavaScript is Synchronous. But there are some specific APIs in the language which are Asynchronous by nature. Like here we’ve used fetch API. Meaning  —  they will get skipped in the main thread and processed later.

It’s a good thing because otherwise this program would freeze until the data is available for us.

But this is also problematic because it’s not a regular way of writing code, there’s an overhead of keeping async things in the sync. For this, we have a much cleaner API now  —  Async/Await. Which also blocks, but you get to control where and when you want to block.

Another thing we want to take advantage of is  —  Parallel Execution (Concurrent precisely). In our previous example,  if we had multiple fetching operations, they would happen in parallel. Thanks to the multi threading interface of Operating System.

To understand this let’s look at another example —

Say we want to read text from 2 different files. There’s an async API in Node to read files. But now we are NOT going to use the actual API, rather some pseudo code.

readFile("fileOne.txt") // say this might take 3 seconds to process

readFile("fileTwo.txt") // and this takes 5 seconds

They are independent operations. So  both data should be available after in total 5 seconds but not 3 + 5 = 8 seconds. So here is the blocking/non parallel version.

Not Parallel  —  takes 8 seconds

readFile("fileOne.txt")
  .then( text => {
     console.log("text from file one", text)

     readFile("fileTwo.txt")
       .then( text => console.log("text from file two", text) )
   })

console.log("Processing...")

There’s is an unnecessary blocker, right ? We don’t want that. Or maybe we would, if we had data dependency like —  the second async operation relies on first one’s resolved value.

Let’s try using async/await.

Not Parallel  —  takes 8 seconds.

async function readAsyncFiles() {
  let text1 = await readFile('/fileOne.txt')
  console.log("text from file one", text)

  let text2 = await readFile('/fileTwo.text')
  console.log("text from file two", text)
}

readAsyncFiles()

console.log("Processing...")

This looks nice, but this is blocking code too. Because await just freezes until the Async action is done.

So how to write this in parallel ?

Parallel —  takes 5 seconds

readFile("fileOne.txt")
  .then( text => console.log("text from file one", text) )

readFile("fileTwo.txt")
  .then( text => console.log("text from file two", text) )

console.log("Processing...")

The output would be in this order -

Processing
text from file one <text>
text from file two <text>

That’s cool.

But what if you want to print “Done” when both of them get finished. Think about it. That’s a bit tricky, isn’t it ?

One option is how we already wrote this, but not parallel.

async function readAsyncFiles() {
  let text1 = await readFile('/fileOne.txt')
  console.log("text from file one", text)

  let text2 = await readFile('/fileTwo.text')
  console.log("text from file two", text)

  console.log("Done")
}

readAsyncFiles()

Another option, printing “Done” inside each then block. Then we would get two notifications for each ,  which is fine, but that is not our purpose.

Instead of printing “Done” what if we wanted to Concatenate the text from both files. I mean we had to write some hacky code to achieve that.

That’s why we have  —  Promise.all(). This API handles multiple independent Async operations in paralllel. And we can await for the whole process to finish and go to the next line eventually.

Parallel  —  takes 5 seconds.

// wrapped in async function

const [text1, text2] = await Promise.all([
                          readFile('/fileOne.txt'),
                          readFile('/fileTwo.txt')
                        ])

console.log("Done")

Here, both file reading operations are parallel and also we get resolved values in sequence. This is great.

Except, this API short circuits. If any of these operations fails, the whole thing fails from that point. What if we want it to work as Microservice , meaning  -  an Async operation can fail, but we still want other operations’ resolved values, then we can’t Promise.all(). Instead we need to use Promise.allSettled().

So now, we have this basic idea that there can be different requirements for Async operations and for handling them there are different variances of Promise API too. For example another useful one is Promise.race().

Event Loop

A promise can have 2 states. Pending and Resolved /Rejected.

A pending promise means  —  it’s currently being handled in the background.
A resolved promise means  —  it will be executed at the end of the running event loop.

On each iteration of event loop, we can consider 3 cases -

  1. If it’s a synchronous code, execute it.
  2. If it’s a pending Promise then skip it. It’s running in the background.
  3. If it’s a resolved(rejected) Promise, then the callback will run at the end of this particular iteration of event loop.

When the resolved Promise is available, it’s then-able. Meaning   we can attach a callback to work with the resolved data. So a resolved Promise can be available anytime inside a particular iteration of Event Loop. And the callback will be fired within this same iteration, but at the very end after finishing all the Synchronous works.

Let’s look at an interesting case -

setTimeout(()=> console.log('timeout'), 0)

Promise.resolve().then(()=> console.log('resolved promise'))

console.log('synchronous')

We are emulating a resolved Promise here and also a timer. So on a running event loop phase, after finishing all the Sync code, it’s going to check

  • If there’s any callback of resolved Promise to run.
  • If there’s a timer callback to run.

So before the timer callback, it’s going to check if there’s any resolved promise. First they are going to be executed. It doesn’t matter how much time it takes, and in the mean time, there could be other resolved promises popped up in the current event loop. After finishing all of them, the timer callback gets executed finally.

That means, you can’t expect the timer to run after the exact interval you provided, like here we did- 0 ms. It might take longer than that.

So output of the code —

synchronous
resolved promise
timeout

N.B. Different browsers can have different implementations. This is Chrome/Node standard behavior.

To understand how event loop actually works- read this- https://nodejs.org/uk/docs/guides/event-loop-timers-and-nexttick/

And a fantastic article by Jake Archibald on Task, Microtask scheduing -

https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

That’s all folks. Have fun with your Asynchronous journey.

Posted on by:

Discussion

pic
Editor guide