Node.jsV8MemoryJavaScript

Inside the Heap: Where JavaScript Stores Everything That Matters

A deep dive into V8's heap memory, where objects, closures, promises, and everything dynamic lives. Understanding what goes in the heap and why.

In the last post, we explored the stack, the region of memory that handles execution. Now we turn to its counterpart: the heap, where data persists beyond function calls.

This is where JavaScript stores things that:

  • Don’t have a fixed size
  • Can outlive a single function call
  • Need to grow, shrink, or be shared

In Node.js (via the V8 engine), the heap is where the interesting stuff lives. Objects, closures, promises, anything that makes JavaScript feel dynamic ends up here.

Let’s walk through what actually goes into the heap and why.

Objects

Any JavaScript object, no matter how small, lives in the heap.

let obj = { a: 1, b: 2 };   // object → heap

Even if an object has a single property, it still goes to the heap.

Why?

Because objects are dynamic. Properties can be added, removed, reordered, or mutated at runtime. The stack cannot handle that kind of flexibility.

Internally, V8 uses hidden classes (also called shapes) to track object structure and optimize property lookups. But regardless of these optimizations, the object still lives in the heap.

Objects live in the heap. Always.

Arrays

Arrays are objects with a numeric indexing convention.

let arr = [1, 2, 3];

Even though the elements are primitives, the array itself lives in the heap.

Important nuance:

  • The array structure is heap-allocated
  • The elements may be stored inline or separately depending on V8 optimizations
  • Dense arrays, sparse arrays, and mixed-type arrays are handled differently

But from a mental-model perspective: if it’s an array, it’s in the heap.

Functions (as Objects)

Functions in JavaScript are not just executable code. They are full-fledged objects.

A function carries:

  • Executable bytecode
  • A .prototype object
  • Metadata
  • Possibly a closure
function greet() {}
const x = greet;

What’s happening:

  • greet is a heap-allocated function object
  • x is just a reference pointing to it

Every function (named, anonymous, arrow) lives in the heap.

This is why functions can:

  • Be passed around
  • Be stored in variables
  • Be returned from other functions
  • Carry state

Execution uses the stack. Functions themselves live in the heap.

Closures

Closures are where heap behavior becomes visible.

A closure is a function bundled together with variables from its lexical environment that it still needs access to.

function outer() {
  let count = 0;   // primitive
  return function inner() {
    count++;
    return count;
  }
}

What’s interesting here is what happens to count.

Normally, count would live in the stack frame of outer. But inner() still needs it after outer() has returned.

So V8 does something clever:

  • It creates a heap-allocated context object
  • count is stored in that context instead of the stack frame
  • inner() holds a reference to that context

This is why closures can retain state indefinitely.

This is also why closures are one of the most common sources of accidental memory retention.

Buffers

Node.js Buffer objects are heap-allocated structures that wrap raw binary data.

const buf = Buffer.alloc(1024);

Under the hood:

  • The Buffer object itself lives in V8’s heap
  • The binary data may be stored in V8’s heap (for small buffers) or in external memory (for large buffers)
  • V8 tracks both to ensure proper cleanup

This design balances performance with memory safety. Buffers are powerful because they bridge managed and unmanaged memory, but that power requires careful handling.

Promises, Maps, Sets, Errors

All higher-level abstractions are heap-based.

This includes:

  • Promise
  • Map
  • Set
  • WeakMap
  • WeakSet
  • Error objects

Even a simple Promise:

new Promise(() => {});

creates heap objects for the promise itself and its internal state.

Heap usage grows not because JavaScript is slow, but because these abstractions are real, heap-allocated objects.

Why the Heap Exists at All

The heap exists because JavaScript allows:

  • Objects to grow dynamically
  • Data to outlive function calls
  • Shared mutable state
  • Asynchronous execution
  • Closures and higher-order functions

None of this is compatible with stack-only memory.

The heap is slower than the stack, yes. But it enables the language. So when should you expect something to live in the heap?

The Rule of Thumb

Here’s the mental shortcut that actually works:

If it can change shape, grow, shrink, or outlive a function call, it belongs in the heap.


The stack handles execution. The heap handles existence.

The heap is what makes JavaScript expressive. But it comes with a problem.

The stack cleans itself up automatically. The heap doesn’t. Objects stick around. Closures hold references. Data piles up.

So how does V8 know when it’s safe to free memory? How does it decide what’s garbage and what’s still needed?

That’s the job of garbage collection—and that’s exactly what we cover in Garbage Collection in V8: How Memory Is Actually Reclaimed.