In the previous post, we talked about Asynchronous Javascript. In this article you will learn:
- What are promises?
- What is fetch API?
- How and why fetch works differently than other web APIs like setTimeout?
- What is Microtask Queue and how it is different from Callback Queue?
What is a Promise?
A promise is a special kind of object in javascript that lets asynchronous methods return values like synchronous methods, i.e, it gets returned immediately when we make a call to a web API like fetch
to supply the final value at some point in the future.
We also attach functionality to the promise object that gets automatically triggered when that value comes back in the javascript from the browser API and that value would also be the input of that function.
What is fetch API?
Suppose you are building a Weather application that displays the weather of a city/country on search. Let's say the UI is done, now comes the main part - the weather data. Here's what you might need -
- The app should be able to get the data from a server.
- When the data arrives, the app should display it.
- If the data fails to arrive, you would still want to display something like an error message.
Javascript doesn't know how to interact with the outer world a.k.a Internet. You know from the Asynchronous Javascript post that this is where Web APIs shine.
Fetch is a Web API that provides an interface for fetching data. Let's see how fetch works.
Why fetch is different than setTimeout?
Fetch works differently than the web APIs we discussed earlier like setTimeout
because it not only initiates a background browser functionality but also immediately returns a placeholder object known as Promise
.
How fetch works under the hood?
Consider the code below:
function display(data){
console.log(data);
}
const upcomingData = fetch('https://getWeatherData.com/temp/seattle');
upcomingData.then(display);
console.log('Me first??');
upcomingData
would be the return value of the fetch('https://getWeatherData.com/temp/seattle')
Fetch returns an object with the initial value undefined
and another hidden property onFulfilled
that is an empty array. This special object runs all the functions provided inside the onFulfilled
array when the value gets updated.
Fetch sets up XHR i.e, XMLHttpRequest in the browser which retrieves the data from the URL. Now while fetch was getting the data from the getWeatherData
servers, javascript continues to execute the next line of code because the task was asynchronous.
Now on the next line, we have upcomingData.then(display)
, if I were to ask you what this line is doing, you would most probably tell that 'when we get upcomingData, then call display function'. At least this was what I had in my mind before. But this is a wrong explanation.
Remember earlier I said that there is a hidden array called onFulfilled
. Well, the method then
will take the argument display
which is a function definition, and push it inside the onFulfilled
array, and on updating the placeholder value of the promise object, all the functions inside the onFulfilled
array will get run automatically.
🤔 Quick doubt - Why use then
, why not onFulfilled.push()
?
Because it's not a normal array, remember it's a hidden array. That's why you can't use the push()
method on the onFulfilled
array.
Now on the next line, Me first??
would be logged to the browser console. Now all of our synchronous code, but the fetch was doing its background work to get the temperature of Seattle city from the server. Let's say it comes back with a response with a value of 12°C
, which updates the value of upcomingData object to 12°C
, which triggers display
function to run with input 12°C
. Hence, now 12°C
is logged to the console.
This is how the output would look like:
Me first??
12°C
What if the server fails to send back the data?
Just like onFulfilled
, there is onRejected
array also, which auto triggers any function inside it when the promise is not resolved. And you would pass the error handling function inside of onRejected
through another method called catch()
. For example:
upcomingData
.then(display)
.catch(handleRejected);
This is known as promise chaining.
That's how fetch works under the hood. Now since you know that, let's see one more example:
function display(data){
console.log(data);
}
function printHello(){
console.log('Hello');
}
function blockFor300ms(){
// blocks js thread for 300ms with long for loop
}
setTimeout(printHello, 0);
const upcomingData = fetch('https://getWeatherData.com/temp/seattle');
upcomingData.then(display);
blockFor300ms();
console.log('Me first??');
Our first 3 blocks of code are 3 function definitions, so javascript will declare the functions and store their entire definition in the memory line by line.
Now we hit our first obstacle, setTimeout
which starts a timer in the browser for 0 ms, which is completed instantaneously. Hence, at 0ms, the function printHello
is added to Callback Queue and setTimeout
's work is done, but the Event Loop won't let it run because there is still out global code to run.
Now, on the next line, we have upcomingData
, so javascript would declare a constant upcomingData
and make it undefined
because its value is not defined yet, i.e, fetch hasn't returned a value yet. Fetch initially returns an empty object a.k.a Promise with the value undefined
and onFulfilled empty array. But its work isn't done yet because it will also do some background browser work of getting the data from the server and when it retrieves the data, it will update the value property on the promise object.
The data retrieving work was going on in the background while we execute the next line of code, we are calling then()
on upcomingData
and passing in the display
which will pass the display
inside the onFulfilled array and whenever the value of the promise object gets updated, the display
function would be triggered to run. But it isn't called yet.
Now the next line of code will block the further code execution for 300ms, i.e, it would be pushed on the Call stack for 300ms.
While it was there, fetch gets back the data from the servers and updates the value of the promise object to 12°C
which triggers display
to run. But it is not pushed into the Call Stack directly just like the callback of setTimeout
. It is queued into a Queue. Well, it turns out like there is a Callback Queue that we use to queue the callback of setTimeout
, there is another queue known as Microtask Queue in which functions triggered by updating the value property of promise object get queued.
Not after 300ms, the function blockfor300ms
is popped off the Call Stack, and the code on the next line gets executed, which logs Me first??
onto the browser console, and all of our synchronous code is finished executing.
There are two queues with functions waiting to be executed, now comes the question:
🥇 Which will run first?
It turns out that Event Loop gives precedence to Microtask Queue over Callback Queue. It sees display
waiting inside the Microtask Queue, hence it is dequeued and pushed into the Call Stack first, and 12°C
is logged to the console.
Now since the Microtask Queue is empty, Event Loop will check the Callback Queue, and then push printHello
into the Call Stack and Hello
is logged to the console window.
This is how the output of the code would look like:
Me first??
12°C
Hello
It's surprising how printHello
gets called at the last even though it was the one waiting from the beginning. And here's an interesting thing you can do, you can never let printHello
run by continuously queuing the Microtask Queue and starve the Callback Queue.
This is how fetch works under the hood and why is it different from setTimeout. If you understand this, there is pretty much nothing left in the asynchronous javascript.
If you are still with me, I want to thank you for reading it. Let me know in the comments if there's something you would like to discuss, or reach out to me on Twitter @AyushCodes, my DMs are open for you.
Happy hacking!!!
Top comments (0)