Skip to content
ExponaBLOG
JAVASCRIPT & TYPESCRIPT · DAY 002 / 100

How JavaScript runs for JavaScript & TypeScript developers

Engine, call stack, heap and event loop.

Expona Admin8 min read

Most JavaScript developers write code that works without really understanding how JavaScript runs. You know callbacks execute, promises resolve, and async/await lets you avoid callback hell—but what's actually happening under the hood? Understanding the engine, call stack, heap, and event loop transforms you from someone who writes code that happens to work into someone who debugs performance issues, avoids race conditions, and designs systems that scale.

The Runtime: The Complete Picture

When you run JavaScript, you're not just executing code linearly from top to bottom. Instead, several components work together in a coordinated dance. Let me walk you through what actually happens.

First, the engine (V8 in Chrome and Node.js, SpiderMonkey in Firefox) parses your code and compiles it into machine instructions. Then the call stack executes synchronous functions one at a time. Objects and variables live in the heap, a memory region managed separately from the stack. When you make async calls—timers, network requests, file I/O—the Web APIs (in browsers) or libuv (in Node.js) handle them outside the engine. Finally, the event loop watches for completed async work and schedules callbacks back onto the call stack.

Here's a concrete example showing all these pieces interacting:

typescript
console.log('Start');

setTimeout(() => {
  console.log('Timeout callback');
}, 100);

fetch('https://api.example.com/data')
  .then(res => res.json())
  .then(data => console.log('Data:', data));

console.log('End');

Execution order:

  1. Engine executes console.log('Start') on the call stack → prints immediately
  2. Engine encounters setTimeout → passes it to the Web API (not the call stack)
  3. Engine encounters fetch → passes it to the Web API
  4. Engine executes console.log('End') on the call stack → prints immediately
  5. Call stack is now empty
  6. After 100ms, the timeout callback is ready; after the network response arrives, the promise callbacks are ready
  7. Event loop sees the call stack is empty and pushes the next callback onto it

Output: Start, then End, then eventually Timeout callback and Data: ....

Single-Threaded Execution: The Call Stack

JavaScript has one call stack. This is both its greatest strength (simplicity, no race conditions) and its biggest weakness (blocking). When a function runs, it occupies the stack. While it's running, nothing else can run.

Consider this scenario from a real e-commerce system I built:

typescript
function calculateCartTotal(items: CartItem[]): number {
  let total = 0;
  // Simulating expensive computation
  for (let i = 0; i < 100_000_000; i++) {
    total += items[i % items.length].price;
  }
  return total;
}

function handleAddToCart(item: CartItem) {
  const newTotal = calculateCartTotal(items);
  updateUI(newTotal);
}

If this calculation takes 2 seconds, the UI thread is blocked for 2 seconds. Users can't click buttons, scroll, type into inputs—nothing. The browser itself feels frozen because the call stack is occupied and the event loop can't process user interactions.

This is why long-running tasks need to be broken up or offloaded. In production systems, I've handled this by:

  • Breaking work into smaller chunks that yield control back: setTimeout(() => processNextChunk(), 0)
  • Using Web Workers (in browsers) or worker threads (in Node.js) for CPU-intensive work
  • Deferring non-critical updates and using techniques like debouncing and request animation frame

The call stack's single-threaded nature is not a limitation of JavaScript itself—it's a deliberate design choice that eliminates entire categories of concurrency bugs.

Memory Management: The Heap

While the call stack stores primitive values and function references, the heap is where objects, arrays, and strings live. The heap is dynamically allocated and managed by garbage collection.

typescript
let primitiveNumber = 42;           // Stored on stack
let objectReference = { name: 'Aj' }; // Reference on stack, object in heap
let arrayData = [1, 2, 3, 4];         // Reference on stack, array in heap

In my trading platform work, understanding the heap became critical. We had thousands of price updates per second coming through WebSockets. Creating a new object for every update would exhaust memory quickly.

typescript
// ❌ Memory pressure: creates 1000s of objects per second
function handlePriceUpdate(symbol: string, price: number) {
  const update = { symbol, price, timestamp: Date.now() };
  broadcasts.push(update); // Heap fills up
}

// ✅ Better: reuse object pool
class PriceUpdatePool {
  private pool: Array<{symbol: string; price: number; timestamp: number}> = [];
  
  getUpdate(symbol: string, price: number) {
    const obj = this.pool.pop() || {};
    obj.symbol = symbol;
    obj.price = price;
    obj.timestamp = Date.now();
    return obj;
  }
  
  release(obj: any) {
    this.pool.push(obj);
  }
}

The garbage collector runs automatically, but understanding that the heap stores objects helps you avoid memory leaks (forgotten event listeners, circular references, detached DOM nodes).

Async Is the Environment, Not the Language

Here's a key realization: JavaScript itself is synchronous. The environment (browser or Node.js) provides asynchronous capabilities. Timers, network requests, and file I/O don't happen in the engine—they happen in the environment's APIs.

In Node.js, libuv is the library that provides this. It manages thread pools for file I/O, DNS lookups, and other blocking operations. In the browser, the Web APIs (fetch, setTimeout, events) offload work to the browser's internal threads.

typescript
// JavaScript engine: synchronous
// libuv/Web APIs: asynchronous

console.log('1');

fs.readFile('./data.json', 'utf-8', (err, data) => {
  // This callback runs later, after file I/O completes
  console.log('3');
});

console.log('2');

// Output: 1, 2, 3

When fs.readFile is called, Node.js immediately returns and adds the task to libuv's thread pool. The JavaScript engine continues. Once the file is read, libuv signals the event loop that the callback is ready.

This separation is why Node.js can handle thousands of concurrent connections with a single thread—I/O doesn't block the JavaScript thread.

The Event Loop: Orchestrating It All

The event loop is a loop that repeatedly checks: "Is the call stack empty? If yes, are there any callbacks ready to run?" If both are true, it pushes the next callback onto the call stack.

The event loop in Node.js runs in phases:

Phase What runs Example
timers setTimeout, setInterval callbacks Your 100ms timeout
pending callbacks I/O error callbacks Network errors
idle, prepare Internal only —
poll I/O callbacks (fs, net) File read, socket events
check setImmediate callbacks Your immediate() call
close callbacks Socket close, stream end Connection cleanup

In browsers, the model is simpler: macrotasks (timers, events), then all microtasks (promises, queueMicrotask), then rendering, repeat.

typescript
console.log('Script start');

setTimeout(() => console.log('setTimeout'), 0);

Promise.resolve()
  .then(() => console.log('Promise 1'))
  .then(() => console.log('Promise 2'));

console.log('Script end');

// Output:
// Script start
// Script end
// Promise 1
// Promise 2
// setTimeout

Promises execute in the microtask queue, which runs before the next timer. This is critical knowledge for performance optimization and avoiding race conditions.

JIT Compilation: Optimizing Hot Code

Modern engines like V8 don't just interpret code—they compile it. Initially, code is interpreted. But if a function is called many times (it's "hot"), the engine's JIT (Just-In-Time) compiler generates optimized machine code based on the types it has observed.

typescript
function add(a: any, b: any) {
  return a + b;
}

// First calls: interpreted, handles any type
add(5, 10);        // 15
add("hello", " world"); // "hello world"
add(3.14, 2.86);   // 6.0

// After many calls with consistent types:
// V8 optimizes assuming a and b are always numbers
add(100, 200);     // Compiled code: very fast

This is why TypeScript is valuable in performance-critical code. By specifying types, you give the engine confidence to optimize. It's also why polymorphic code (functions that handle wildly different types) can be slow—the engine can't optimize as effectively.

In my CRM platform, I noticed that sorting large arrays of users was slow until I ensured the comparator function had consistent type handling. The engine could then optimize aggressively.

Common Mistakes

  • Blocking the event loop: Running long synchronous operations prevents the event loop from processing other callbacks. Always measure and break up work.
  • Forgetting the call stack is single-threaded: Race conditions aren't impossible in JavaScript (Web Workers can cause them), but the default single-threaded model makes them rare. Don't assume you have parallelism.
  • Misunderstanding microtasks vs. macrotasks: Promise callbacks run before timers. If you expect setTimeout to run first, you'll debug for hours.
  • Memory leaks from retained references: Callbacks that capture large objects can keep those objects in the heap. Clean up listeners and use weak references where appropriate.
  • Assuming async/await magically solves performance: It makes code readable, but await still blocks. A slow I/O operation is still slow, whether you use callbacks or async/await.

Key Takeaways

  • JavaScript runs through an engine (parser, compiler), call stack (executes functions), heap (stores objects), and event loop (orchestrates async callbacks).
  • The call stack is single-threaded—long tasks block user interactions and other callbacks.
  • Async operations (timers, I/O, network) happen in the environment (Web APIs, libuv), not the engine. Callbacks rejoin the call stack through the event loop.
  • The event loop runs in phases (timers, I/O, setImmediate, etc.). Microtasks (promises) run before macrotasks (setTimeout).
  • V8's JIT compiler optimizes hot code based on observed types. Stable types enable aggressive optimization; polymorphic code stays slow.
  • Understanding these mechanics helps you debug performance issues, avoid blocking, manage memory, and design systems that handle load.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles