The word "Asynchronous" means something will happen in the future without blocking other tasks.
Let's say we wrote some instructions with JavaScript.
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
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
. 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. 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.
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.
async function readFiles() {
let text1 = await readFile('/fileOne.txt') // 3 seconds
console.log("text from file one", text)
let text2 = await readFile('/fileTwo.text') // 2 seconds
console.log("text from file two", text)
}
readFiles()
console.log("Processing...")
This looks nice, but this is blocking code. They are independent operations. So they should take only 3 seconds to process. But now they are taking 3 + 2 = 5 seconds.
So how to write this in parallel ?
Promise.all() - this API handles multiple independent Async operations in paralllel. And we can await
for the whole process to finish.
const [text1, text2] = await Promise.all([
readFile('/fileOne.txt'),
readFile('/fileTwo.txt')
]) // total 3 seconds
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 use 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 -
- If it’s a synchronous code, execute it.
- If it’s a pending Promise then skip it. It’s running in the background.
- 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.
Top comments (0)