Did you ever wonder what's going on behind the scenes when you run a very small piece of code in JavaScript? I actually didn't for a long time.
I wanna understand it with you right now. Let's try to discover together! Hop on the bus, starting! 🚎
I offer to write a very very small code block and try to understand its story:
const totalApples = 10;
const totalBananas = 5;
function getTotalFruits() {
return totalApples + totalBananas;
}
const totalFruits = getTotalFruits();
Cool, we all know that result is 15 here. So easy. But what happened under the hood?
Before we start, keep in mind that JavaScript is single threaded and synchronous language:
- It can only execute one thing at a time.
- It executes them line by line from top to bottom.
FIRST OF ALL, Global Execution Context is created by JavaScript Engine.
JavaScript Engine
Javascript Engine is a program that executes the javascript code.
Execution Context
Imagine that: Execution context is a two-sided place where JS code is declared and executed. Imagine a board which has two sides.
On the right side (Memory), all variables and functions are declared.
On the left side (Thread of Execution), code is executed line by line.
There are 2 types of Execution Context:
Global Execution Context (GEC)
Whenever JS Engine executes a script, it creates a global execution context which all global code handled.
Function Execution Context (FEC)
Whenever a function is called, it creates a function execution context which has its own local memory and thread of execution.
LET'S TRY TO VISUALIZE:
Remember our code:
// Step 1
const totalApples = 10;
const totalBananas = 5;
// Step 2
function getTotalFruits() {
return totalApples + totalBananas;
}
// Step 3
const totalFruits = getTotalFruits();
Script executed, and Global Execution Context created:
Step 1:
Declare totalApples and totalBananas variables in the memory.
Step 2:
Declare getTotalFruits function in the memory.
Did you realized you can also save a function in the memory? Let's go on.
Step 3 (Part 1):
Declare totalFruits variable in the memory. But do I know value of this variable? NO! We cannot save commands in the memory. So it will be saved as uninitialized at the moment. See it:
Step 3 (Part 2):
Time to execute our function on Thread of Execution. AND FOR THE FIRST TIME HERE, Function Execution Context comes into our live. Here it is:
Is that all? NO my friend! Something is missing here. A new term reveals in this step. Whenever you call a function, it pops in Call Stack.
Call stack
Call Stack is where the JavaScript Engine keeps track of where your code is in the execution.
You can basically see which functions is running right now in the Call Stack.
🎤 Imagine that you are a singer. Announcer is calling your name. You are coming and singing your song. When the song is over, you are getting off the stage. Stage is Call Stack here. 🎤
When a function is called, it pops in Call Stack. And it pops out when it has done its job.
Global Execution is running at the bottom as default in Call Stack.
Function called:
Function finished its job:
Note that: There is ONLY ONE Call Stack since JavaScript is a single threaded language.
HUH! I guess it is done, right? Please warn me, if I do anything wrong. Let's keep up the work.
It looks okay like this, but, what if we add some complexity to our function?
Let's not be afraid and do it!
It looks very clean when it goes synchronously. But what will happen when some functions are asynchronous?
We can basically do it with setTimeout:
const totalApples = 10;
const totalBananas = 5;
function getTotalFruits() {
return totalApples + totalBananas;
}
function runTimer() {
console.log('timeout run!')
}
const totalFruits = getTotalFruits();
setTimeout(runTimer, 0);
console.log(totalFruits);
So guys, here is interesting. New terms reveal here again.
Did you know that JavaScript doesn't have a timer? Also console?
Everybody sometimes needs a support, so does Javascript. WEB APIs help JavaScript here.
Web APIs
Web APIs are generally used with JavaScript and add some power to our functionality. Timer, Console, Network requests and many other things are Web APIs' skills, not Javascript.
JavaScript just communicates with browser via some labels like setTimeout.
Let's Visualize
Assume that, it is setTimeout's turn in the Thread of Execution and let's try to visualize this step:
hmm, weird things are happening. Let me try to summarize it.
Please some attention now:
Functions and variables saved in memory as usual. They are executed in the thread. And when it is setTimeout's turn, JavaScript didn't send setTimeout's callback (runTimer) into Call Stack. Instead, it communicated with Web Browser and sent a message like:
''Hey buddy 👋 Can you start a timer for me? And when timer is completed, here is your callback to run (runTimer).''
As you see, after 0ms, timer is completed, callback is ready to run BUT guys, Thread of Execution never stops, exactly like life.
In the time when Web Browser handles timer, functions kept executing in the thread and even if callback(runTimer) is ready to be executed, Call Stack is not empty anymore! And remember: There can be only one execution at a time. BUT I HAVE TWO FUNCTIONS TO RUN IN MY HANDS.
What is gonna happen? Is JavaScript broken?!?!!?
Please no, we need it.Or will JavaScript run my functions in a random order?
That would be very unpredictable and disaster.
There should be a better solution.
🚨 NEW TERM ALERT 🚨
Our callback is waiting in Callback Queue.
Callback Queue
Callback Queue is kind of a waiting room for callbacks which are ready to be executed. Callback is waiting here for the Call Stack to empty. Whenever is Call Stack is empty, our callback pops in Call Stack and do its job.
Event Loop
Event Loops basically checks Call Stack continuously, and when Call Stack is empty, let there know about it. And first callback in the queue pops in Call Stack.
Let's add some visuals including Callback Queue and Event Loop
Oh guys, victory! 🏆
Just a little question; What if current execution continues for 1000 seconds or whatever while our callback is waiting in Callback Queue? Is it gonna wait forever?
Sounds unbelievable but yes! Whatever the cost, it is gonna wait until Call Stack is empty. No permission to run unless Call Stack is empty.
Okay! Adventure of this code block completed here, right? But I feel like we can add some more complexity here.
PLEASE DON'T LEAVE ME! I PROMISE THIS IS LAST SECTION. You will not regret, be patient!
What would happen if I need to fetch data from server?
Let's fetch some data:
const totalApples = 10;
const totalBananas = 5;
function getTotalFruits() {
return totalApples + totalBananas;
}
function runTimer() {
console.log('timeout run!')
}
function showData(data) {
console.log(data)
}
const totalFruits = getTotalFruits();
setTimeout(runTimer, 0);
// Fetch some data
const fetchData = fetch('https://jsonplaceholder.typicode.com/todos/1');
fetchData.then(showData);
console.log(totalFruits);
Okay, when we fetch data, we will communicate with Web Browser again. Because we need its APIs here.
I will not mention about details of fetch API, just talk about what is going on in the Thread of Execution, Call Stack and Queue while fetching data.
Hmm, let's try to write down a timesheet about what happened here:
1) Functions and variables saved in the memory, executed in the thread blablabla...
2) setTimeout communicated with Web Browser's timer.
3) setTimeout's callback (runTimer) is ready and waiting in Callback Queue after 0ms. [0ms]
4) fetch is doing two jobs:
a) Communicated with Web Browser's network feature.
b) Created a special Promise Object in the memory via Javascript Engine.
5) fetch's callback (showData) is ready with data to run after 200ms BUT we don't know yet where it is waiting right now. We'll see. [200ms]
6) While all those are happening, console.log(15) popped in Call Stack. AND assume that: it has taken 250ms to console and after 250ms it will pop out from Call Stack. (I know it wouldn't take that time in real life but just an assumption to understand the story). [250ms]
So, console.log(15) is running in the Call Stack, runTimer is waiting in Callback Queue. Where is showData waiting?
Based on our current knowledge, showData should be waiting right behind runTimer in the Callback Queue, right?
No my friend, it is not.
For the last time,🚨New Term Alert🚨:
There is an another kind of queue here besides Callback Queue.
Microtask Queue
Whenever a callback is related to Promise Object in JavaScript Engine, it will be waiting in Microtask Queue such as callbacks of fetch functions.
Note that: Microtask Queue has a privilege over Callback Queue.
First, callbacks in Microtask Queue will pop in Call Stack. After they are done, callbacks in Callback Queue will start to pop in Call Stack.
In our scenario, firstly, showData will run because it is waiting in Microtask Queue. And then, runTimer which is waiting in Callback Queue will run.
Then, final result looks like:
What an adventure! 😓 just a fucking piece of code, crazy.
Our journey ends here, guys. 🚎 thank you for joining me!
Stay in touch with me for more adventures 👇
Follow me on:
Github: https://github.com/inancakduvan/
Twitter: https://twitter.com/InancAkduvan
Thank you for coming with me until end of the reading! 🙂
Top comments (22)
Very nice explanation!
One small addition that I'd like to make is "Hoisting". Even before the code execution begins, all variable and function declarations are hoisted (in simpler terms, moved) to the top of the file.
Looking at your very first example, if we make a small change to the code, like so
Even though it may seem a bit counter-intuitive, the code still runs exactly as before. This is because the function declaration is moved to the top of the file before execution. Note that only declarations are hoisted, and not definitions, so if you're assigning any values to some variables, the assignments still stay in the place where you wrote them, it's just that the declaration statement with an undefined value is moved to the top of the file.
Speaking of declarations vs. definitions, one little caveat that has caught me off guard in the past, is with arrow functions. In the above code
getTotalFruits
is a function, since it is declared with thefunction
keyword, but if we change it to an arrow function,the code throws a
ReferenceError
. This is because,getTotalFruits
is now technically a regular variable which is assigned an anonymous function.Hope you and anyone reading find this useful.
Cheers!
Genius
Very useful addition, Gaurav! You enlightened us. Definitely I benefit from your explanation.
Thank you for your contribution 🤟
Very cool, thanks!
Question: what if we put the
console.log(..)
statement beforefetchData.then(..)
? The call stack is empty afterconsole.log
and even though thefetchData
promise is already done, we haven't putshowData
on the microtask stack, sorunTimer
will be first?Hi, thanks!
If I understand the question, I will try to explain.
Settimeout, console.log, fetch... this is the calling order in your question.
First Scenerio: Fetching takes 200ms time and consoling takes 250ms:
-- setTimeout communicates with Web API
-- console.log is in Call Stack. [0ms]
-- runTimer is ready waiting in Callback Queue [0ms]
-- fetch communicates with Web aPI [1ms]
-- showData is ready waiting in Microtask Queue [200ms]
-- console.log run and consoled and removed from Call Stack [250ms]
-- showData run in Call Stack [251ms]
-- runTimer run in Call Stack [252ms]
Second Scenerio: Fetching takes 300ms time and consoling takes 250ms:
-- setTimeout communicates with Web API
-- console.log is in Call Stack. [0ms]
-- runTimer is ready waiting in Callback Queue [0ms]
-- fetch communicates with Web aPI [1ms]
-- console.log run and consoled and removed from Call Stack [250ms]
-- runTimer run in Call Stack [251ms]
-- showData is ready waiting in Microtask Queue [300ms]
-- showData run in Call Stack [301ms]
I guess, it is something like that. :)
Interesting. The second scenario is clearly correct because the fetchData promise isn't done when console.log finishes, so runTimer gets picked up from the callback queue.
I'm not so sure about the first scenario... Suppose you never call
fetchData.then(showData)
then showData will never run (obviously). Now, suppose you keep the fetchData promise object around for a while, do a bunch of stuff (a for loop logging "hello" to the console a few thousand times), and only then callfetchData.then(showData)
. Surely, therunTimer
callback would have had the chance to get picked up from the callback queue, after one of theconsole.log("hello")
calls in the loop, when the call stack is empty, and there's nothing on the microtask queue yet.My version: Fetching takes 200ms time and consoling takes 250ms:
-- setTimeout communicates with Web API
-- console.log is in Call Stack. [0ms]
-- runTimer is ready waiting in Callback Queue [0ms]
-- fetch communicates with Web aPI [1ms]
-- console.log run and consoled and removed from Call Stack [250ms]
-- runTimer run in Call Stack [251ms]
-- fetchData promise puts showData on microtask queue [251ms]
-- showData is ready waiting in Microtask Queue [251ms]
-- showData run in Call Stack [251ms]
Maybe I should just test it and post the results here :)
You might be right 🙂 but probably, Call Stack will never be empty during the for loop. Therefore runTimer cannot find a chance to jump in Call Stack.
One of the reasons that I guess like that:
for loop is inside Call Stack as a whole code block, so while passing from one console.log to another one, Call stack is not actually empty at any moment.
But as you said, the best way to learn is testing. If you test it, please let us know the results here ✌️
Thank you so much, it's worth reading
Thank you 🙏
Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 🫰
Thank you for sharing 🤟✌️
very well explained. Thanks.
Understanding these basics save you a lot of trouble later. Been there 😅
I totally agree with this 🙂 thank you!
probably the best explanation!
thank you Joshi! 🙂
amazing!
Thank you for sharing!
Amazing! Thank you :)
Thanks! 🙏
Thanks for the explanation, really clear and straight forward.
Glad to hear you liked it, thanks!
Very nice and simple explanation! I like it. ❣️
Glad you like it, thanks Zakariya 🙏