Thread Memory And Call Stack

To become an autonomous engineer, you must look past the syntax and understand the underlying mechanics of how code runs line-by-line.

April 23, 20268 min read

I've found that many of the most frustrating bugs in JavaScript don't come from a lack of knowing the syntax, but from a missing mental model of what's happening under the hood. To become an autonomous engineer, someone who can tackle any new framework or tool, I had to stop guessing and start visualizing the "thread of execution."

The Essentials

  1. Thread of Execution: JavaScript goes through code line by line, executing each instruction in order.
  2. Memory (Variable Environment): A place to store data (like strings or numbers) and functionality (functions) so we can refer back to them later.
  3. Execution Context: A "mini-program" created every time we run a function. It contains its own local memory and its own thread of execution.
  4. Call Stack: The data structure JavaScript uses to track where we are in the program and which execution context is currently active.

The Big Two: Thread and Memory

At its simplest, every program I write does only two things: it saves data in memory and it runs code line,by,line. This concept is closely related to how JavaScript controls the DOM, where the internal memory must be explicitly synced with the browser's view.

JavaScript Execution Engine
Thread of Execution
1const userAge = 25;
2function multiplyBy2(inputNumber) {
3 const result = inputNumber * 2;
4 return result;
5}
6const userScore = multiplyBy2(userAge);

Step 1:We start in the Global Execution Context. JavaScript prepares to run the first line.

Memory
Memory is empty...
Call Stack
Global
Bottom of Stack

Imagine I have this snippet:

JavaScript
const userAge = 25; function multiplyBy2(inputNumber) { const result = inputNumber * 2; return result; } const userScore = multiplyBy2(userAge);

When I "turn on" JavaScript, the first thing it does is create a Global Execution Context. This is the main stage where my code runs. This is similar to the memory isolation we see in object-oriented design, where different instances maintain their own private state.

  1. Line 1: JavaScript sees const userAge = 25. It carves out a spot in Global Memory, labels it userAge, and stores the value 25.
  2. Line 2: It sees the function keyword. It saves the entire code block for multiplyBy2 in Global Memory. It doesn't run the code inside yet; it just stores it.
  3. Line 5: It encounters const userScore = multiplyBy2(userAge). Before it can assign anything to userScore, it has to evaluate the right side. The parentheses () are the signal to go and execute that function.

The Execution Context "Mini-Program"

The moment I call multiplyBy2(userAge), JavaScript creates a new Execution Context. Think of this as a temporary workspace just for this function call.

Inside this context, we get:

  • Local Memory: Where we store the parameter inputNumber (assigned the value of our argument, 25) and the local constant result (which evaluates to 50).
  • Thread of Execution: A new thread that steps through the two lines of code inside the function.

Once the thread hits the return keyword, it sends the value of result (50) back out to the Global Execution Context, where it finally gets assigned to the label userScore.

The local execution context is then deleted, all those local labels are forgotten.

The Call Stack: Keeping Track

If I have functions calling other functions, how does JavaScript know where to go back to when a function finishes? It uses the Call Stack.

JavaScript Execution Engine
Thread of Execution
1const n = 5;
2function square(x) {
3 return x * x;
4}
5const s1 = square(n);
6const s2 = square(10);

Step 1:We've defined 'n' and 'square'. The stack only contains 'Global'.

Memory
n5
squaref
Call Stack
Global
Bottom of Stack
  • As soon as the file starts, Global() is pushed onto the bottom of the stack.
  • When I call square(), it is pushed onto the top.
  • JavaScript always works on whatever is at the very top of the stack.
  • When square() returns, it is "popped" off the stack.
  • JavaScript looks at the new top of the stack and realizes it should be back in the Global() context.

The Lifecycle of the Stack

I've realized that the call stack is more than just a list--it's the heartbeat of the program. If I ever find myself in a "Stack Overflow" error, it's usually because I've pushed too many contexts onto the stack (often through infinite recursion) without ever popping them off.

But when does the stack finally clear? The Global() context stays at the bottom for as long as the file is running. Only when every line of the global thread has been executed does Global() get popped.

However, in modern web development, the stack often stays alive even after the initial script finishes. There might be "things waiting to come back"--like a timer or a network request response. We'll explore this later when we talk about the Event Loop, but for now, remember this rule: if it's on top of the stack, it's the boss.

I've found that by verbalizing this process, literally saying "we are popping this off the stack and returning to global," I can catch logic errors that used to feel like magic.

Now that we understand how the thread moves through our memory, we can start to see why passing functions themselves into this memory can be so powerful. In the next part, we'll look at how Callbacks and Higher Order Functions take this simple execution model and make it extremely flexible.

Further Reading and Watching

Blog post:

Video:

Practice what you just read.

Visualizing the Thread of Execution
1 exercise