Back to Articles
Engineering

How JavaScript Actually Works: Demystifying the Call Stack and Event Loop

9 min read

1. Introduction

JavaScript is the most popular programming language in the world, but interestingly, it is also one of the most hated. This usually happens because many developers write JavaScript without understanding what actually happens when their code runs.

JavaScript looks simple on the surface. But behind the scenes, there are multiple systems working together to execute your code.

If you don't grasp these core fundamentals, you might run into unexpected errors. In fact, 90% of developers do not properly understand these internal concepts, which is the main reason why they fail technical interviews.

This article will help you build a mental model of how JavaScript actually executes code inside the engine.


2. The Big Picture

Before we look at the exact details, let's look at the overall system.

The JavaScript runtime is not just one thing. It consists of several moving parts working together to handle the execution of your code.

These parts include:

  • JavaScript Engine
  • Memory Heap
  • Call Stack
  • Web APIs
  • Callback Queue
  • Event Loop

We are going to explore each piece step by step so you know exactly how things work and can avoid unexpected errors.

All the parts of the JavaScript runtime working together — engine, heap, call stack, web APIs, callback queue, and event loopClick to expand


3. JavaScript Engine

To start, we have the JavaScript Engine.

This engine is the core environment where your code actually runs. Its main job is to take the code you wrote, read it, and execute it behind the scenes.

Without the engine, your browser would just see raw text. The engine translates that text into instructions the computer can perform.

The JavaScript engine — the core environment that reads your source code and turns it into executable instructionsClick to expand


4. Memory Heap

Once the engine starts reading your code, it needs a place to keep track of your data. This is where the memory heap comes in.

The memory heap is simply a large, unstructured pool of computer memory.

Whenever you create an object, array, or variable, the engine asks the memory heap for space to store it. This process is called memory allocation. Think of it as a giant warehouse where JavaScript drops off data so it can find it later when your code needs it.

The memory heap — every object, array, and variable you create gets a slot in this unstructured pool of memoryClick to expand


5. Call Stack

While the memory heap stores data, the call stack tracks the actual actions your code is taking.

The call stack is a basic data structure that records where we are in the program.

When you tell a function to run, it gets placed onto the top of the stack. If that function calls another function, the new function goes on top of the previous one. Once a function finishes executing, it is removed from the top of the stack.

This stack order ensures that the engine processes one step at a time in the exact order it was called.

The call stack — functions go on top when called, come off when finished, one at a time, in strict orderClick to expand


6. Execution Context

Whenever a function goes onto the call stack, it creates something called an execution context.

Every running piece of JavaScript operates inside an execution context. It acts like an isolated container for that specific block of code.

Conceptually, it has two phases:

  1. Creation phase — sets up memory space for your variables and functions before running anything.
  2. Execution phase — actually runs the code line by line and assigns values to those variables.

Execution context — the isolated container created for each function, with a creation phase and an execution phaseClick to expand


7. Single Threaded Nature of JavaScript

JavaScript is a single-threaded language.

This means it has only one call stack and can only execute one task at a time. It processes code exactly how you read a book: one line after another, in strict order.

Because it can only do one thing at a time, long-running tasks can freeze the entire program. If a task takes too long, nothing else can run until it finishes. This limitation is exactly why asynchronous work needs to be handled carefully.


8. Why Asynchronous Behavior Exists

If JavaScript could only run synchronously, a heavy task like downloading a large file would stop everything else from working. The browser would freeze, and users couldn't click buttons or scroll.

To solve this problem, asynchronous behavior exists. It allows the system to start a slow task, set it aside, and keep running the rest of your code.

But since the JavaScript engine itself can only do one thing at a time, it needs outside help to handle that background work.


9. Web APIs

This outside help comes from Web APIs.

Web APIs are extra features provided by the web browser, not the JavaScript engine itself. They handle background tasks like timers, network requests, and listening for user clicks.

When JavaScript encounters an asynchronous task, it hands the work over to the browser's Web APIs. The engine then moves on to the next line of code, while the browser does the heavy lifting in the background.

Web APIs — the browser's background processors that handle slow tasks like timers and network requests while JavaScript moves onClick to expand


10. Callback Queue

When the browser finishes an asynchronous task, it needs a way to tell the JavaScript engine the work is done.

It does this by sending a callback function to the Callback Queue.

The Callback Queue is simply a waiting area. Callbacks wait in line here until the engine is completely free and ready to process them.

The callback queue — a waiting room where completed async tasks line up until the call stack is emptyClick to expand


11. Event Loop

The Event Loop is the bridge between the waiting callbacks and the execution engine.

You can think of it as a constant traffic controller. It runs continuously and asks one simple question: Is the call stack currently empty?

If the stack is empty, it means the engine has finished all of its current work. The event loop will then take the first task waiting in the callback queue and push it onto the call stack to be executed.

The event loop — it watches the call stack and, the moment it empties, pushes the next waiting callback in to runClick to expand


12. Example Walkthrough

To truly understand the Event Loop and asynchronous JavaScript, we need to see how the engine prioritizes different types of tasks.

Let's look at a code example that combines normal synchronous code, a setTimeout, and a Promise.

console.log("Start");
 
setTimeout(function timeoutCallback() {
    console.log("Timeout finished");
}, 0);
 
Promise.resolve().then(function promiseCallback() {
    console.log("Promise finished");
});
 
console.log("End");

Notice that the setTimeout has a delay of 0 milliseconds. You might assume it will run immediately. However, the actual output in the console will be:

  1. "Start"
  2. "End"
  3. "Promise finished"
  4. "Timeout finished"

Let's walk through exactly what happens under the hood step by step.

Step 1: Running the normal synchronous code

The engine reads the first line, console.log("Start"). It pushes this onto the Call Stack, prints "Start", and pops it off.

Step 2: Handling setTimeout

The engine moves to the setTimeout. Even though the delay is 0, this is still an asynchronous Web API feature. The engine hands the timer over to the browser. The browser instantly finishes the 0-second timer and places the timeoutCallback into the standard Callback Queue.

Step 3: Handling the Promise

Next, the engine encounters the Promise. Promises are also asynchronous, but they are handled slightly differently than a setTimeout. When a Promise resolves, its callback function (promiseCallback) does not go to the standard Callback Queue. Instead, it goes to a special VIP waiting area called the Microtask Queue.

Step 4: Finishing the normal code

The engine reaches the final line, console.log("End"). It pushes it onto the Call Stack, prints "End", and pops it off. The Call Stack is now completely empty.

Step 5: The Event Loop checks the queues

Now that the main code is done, the Event Loop steps in. It sees that the Call Stack is empty and looks for waiting callbacks.

Key Rule: The Microtask Queue has higher priority than the regular Callback Queue. The Event Loop will always completely empty the Microtask Queue before it touches anything in the standard Callback Queue.

Step 6: Executing the Promise

Because the Promise callback is waiting in the high-priority Microtask Queue, the Event Loop grabs promiseCallback first and pushes it onto the Call Stack. The engine prints "Promise finished" and pops it off.

Step 7: Executing the setTimeout

The Event Loop checks the Microtask Queue again. It is now empty. Finally, it looks at the standard Callback Queue, grabs the waiting timeoutCallback, and pushes it onto the Call Stack. The engine prints "Timeout finished", and the program is fully complete.

Microtask queue vs callback queue — promises jump the line over setTimeout because microtasks always run firstClick to expand


13. Final Mental Model

To master JavaScript, you must understand how all these pieces work together under the hood.

ComponentRole
JavaScript EngineReads and executes your code
Memory HeapStores your data (objects, arrays, variables)
Call StackRuns your functions one by one in order
Web APIsHandles slow, asynchronous tasks in the background
Callback QueueHolds finished async callbacks until the stack is free
Event LoopMoves waiting callbacks onto the stack when it's safe

By visualizing this system, you will write cleaner code and avoid unexpected errors.

The complete JavaScript runtime — all six components, their roles, and how they pass work to each otherClick to expand