DEV Community 👩‍💻👨‍💻

Cover image for Let me understand how JavaScript works under the hood
İnanç Akduvan
İnanç Akduvan

Posted on

Let me understand how JavaScript works under the hood

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Script executed, and Global Execution Context created:

Step 1:

Declare totalApples and totalBananas variables in the memory.

Image description

Step 2:

Declare getTotalFruits function in the memory.

Image description

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:

Image description

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:

Image description

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:

Image description

Function finished its job:

Image description

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);
Enter fullscreen mode Exit fullscreen mode

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:

Image description

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

Image description

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);
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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:

Image description


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 (20)

Collapse
 
sainig profile image
Gaurav Saini • Edited on

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

const totalApples = 10;
const totalBananas = 5;

const totalFruits = getTotalFruits();

function getTotalFruits() {
   return totalApples + totalBananas;
}
Enter fullscreen mode Exit fullscreen mode

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 the function keyword, but if we change it to an arrow function,

const getTotalFruits = () => {
   return totalApples + totalBananas;
};
Enter fullscreen mode Exit fullscreen mode

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!

Collapse
 
gnio profile image
Gnio

Genius

Collapse
 
inancakduvan profile image
İnanç Akduvan Author • Edited on

Very useful addition, Gaurav! You enlightened us. Definitely I benefit from your explanation.

Thank you for your contribution 🤟

Collapse
 
mrwensveen profile image
Matthijs Wensveen

Very cool, thanks!

Question: what if we put the console.log(..) statement before fetchData.then(..)? The call stack is empty after console.log and even though the fetchData promise is already done, we haven't put showData on the microtask stack, so runTimer will be first?

Collapse
 
inancakduvan profile image
İnanç Akduvan Author • Edited on

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. :)

Collapse
 
mrwensveen profile image
Matthijs Wensveen

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 call fetchData.then(showData). Surely, the runTimer callback would have had the chance to get picked up from the callback queue, after one of the console.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 :)

Thread Thread
 
inancakduvan profile image
İnanç Akduvan Author • Edited on

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 ✌️

Collapse
 
quanghm27 profile image
quanghm27

Thank you so much, it's worth reading

Collapse
 
inancakduvan profile image
İnanç Akduvan Author

Thank you 🙏

Collapse
 
fruntend profile image
fruntend

Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 🫰

Collapse
 
inancakduvan profile image
İnanç Akduvan Author

Thank you for sharing 🤟✌️

Collapse
 
ljq0226 profile image
ljq0226

amazing!
Thank you for sharing!

Collapse
 
dhruvjoshi9 profile image
Dhruv Joshi

probably the best explanation!

Collapse
 
inancakduvan profile image
İnanç Akduvan Author

thank you Joshi! 🙂

Collapse
 
shammisharma profile image
shammisharma

very well explained. Thanks.
Understanding these basics save you a lot of trouble later. Been there 😅

Collapse
 
inancakduvan profile image
İnanç Akduvan Author

I totally agree with this 🙂 thank you!

Collapse
 
damla_kker_f09b6fb1f63f6 profile image
Damla köker

Amazing! Thank you :)

Collapse
 
inancakduvan profile image
İnanç Akduvan Author

Thanks! 🙏

Collapse
 
zakariya09 profile image
Zakariya Khan

Very nice and simple explanation! I like it. ❣️

Collapse
 
inancakduvan profile image
İnanç Akduvan Author

Glad you like it, thanks Zakariya 🙏

🌚 Life is too short to browse without dark mode