handle———▶pointer

00 Explorable explanation · indirection & safety

A handle is a
pointer you can't hold.

You hold a small reference in one world. The real thing — a raw memory address of a struct — lives in another, and the runtime won't let you touch it. So it keeps the pointer in a private table and hands you a stand-in: a bare key, or that key dressed up as a wrapper object with methods. Either way, every call rides the same path down through the table, to the pointer, to the resource — and brings a plain value back. Only the raw pointer skips the membrane. Press play, click any node, and open under the hood for the C concretes.

▶ The resolution round-trip · key → table → pointer → resource → back
00 / 08
FRAME 00 · —

Press play to trace one call across the boundary

The amber lane is the only place your script lives. Everything teal is owned by the runtime and native code — reached only through the key you were handed.

Script-owned · the handle (a key) Runtime / native · table · pointer · resource The raw pointer · undefined behaviour

01 The mental model

A lightweight value standing in for a real resource in another world.

The handle is not the thing itself; it's a controlled doorway to it. You hold it and pass it around in script, and the methods on it cross the boundary into native code to do the actual work. Underneath, the resource really is a pointer — a raw memory address of a C/C++ struct. Handing that address to your code would be unsafe: you could corrupt memory, or it could move or be freed underneath you.

So the runtime keeps the real pointer in a private table and hands you an opaque stand-in instead. That stand-in comes in two shapes: a bare key — often a small integer — or a wrapper object that carries the same key but wears methods and properties. Every call passes it back; the runtime resolves it to the pointer and dereferences it for you. That extra hop — handle → table → pointer → resource — is what buys safety, whichever shape you hold.

the recurring pattern a small reference lives in your world  →  the runtime resolves it to a real pointer  →  native code does the work on the resource  →  a plain value comes back.

The pattern shows up everywhere a resource is owned by a lower layer. You always get a small reference, never the underlying memory:

What you holdThe opaque keyThe real thing underneathOwner
File descriptora small int (e.g. 3)kernel struct file* in the open-file tableOS kernel
GPU texture handlean opaque ID / objectVRAM allocation + driver stateGPU driver
DB connectiona connection objectsocket + session struct in the pooldriver / pool
fetch() / DOM nodea JS wrapper objectC++ Blink object on the native heapbrowser engine
OS timer (setTimeout)a numeric timer idkernel/loop timer entryevent loop
the one-line version A handle is an indirection with a safety membrane: handle → (runtime's table) → real pointer → resource.

02 The indirection, live

One resource. Three ways to hold it.

This is the whole idea made tangible. Open a file to mint a resource: a struct appears in native memory at some address, a row is inserted in the runtime's private table, and you get back a reference. Then switch how you hold that reference. As a bare key the runtime resolves it for you. As a wrapper object the same key wears methods — a curated, enforceable surface. As a raw pointer the address lands in your hand with nothing between you and the memory. Open, read, and free in each mode and watch what changes — and what doesn't.

① your script JS

you hold opaque keys — just values

② runtime handle table PRIVATE

key → real pointer · you can't see this

key→ pointer (address)
③ native memory C / C++

the real struct File* at an address

▸Open a file to mint your first resource. Nothing exists yet — no struct, no pointer, no key.
read it off the screen Key and wrapper share the middle column — both resolve through the runtime's table, so neither ever holds an address. The only difference is the surface: a bare number versus an object with methods that can refuse you (closed, read-once). Raw pointer deletes that whole column — the address is in your hand, and freeing turns it into a live grenade.

03 Pointer · direct & dangerous

A value that holds the address of something else.

Dereferencing a pointer (*p in C) follows the address to read or write the real data. It's direct and powerful — and dangerous: nothing stops you reading freed memory, walking off the end of an array, or aiming it at garbage. These are the bugs managed languages exist to prevent. Drive the raw pointer below and trigger each one.

▸This is your array. The arrow is a raw pointer p aimed at arr[0]. The teal cells are memory you legitimately own.
why this is the whole problem Every red outcome above is undefined behaviour. The CPU happily follows the address; there is no bounds check, no liveness check, no owner. A handle exists precisely so your code can never be that arrow.

04 The safety membrane

A handle is a pointer with a membrane around it.

Both paths end at the same struct. The difference is the hop. The raw pointer is a single arrow from your hand into memory — fast, and fatal when the target moves or dies. The handle inserts the runtime's table between you and the address: an indirection that can validate the key, relocate the resource, or reference-count it, all without your reference ever breaking. Toggle to compare.

⚠ Raw pointer · direct dereference tap any box for an interactive detail ↗
PropertyRaw pointerHandle (key)Wrapper (object)
What you holdthe actual addressan opaque keyan object wrapping the key
Indirectionnone — one hopone hop through a tablesame hop — object resolves the key
Surfaceraw memory, no APIa value; methods take the keymethods & properties on the object
Use after freesilent corruption / crashkey no longer resolves → clean errorobject refuses → throws (closed / read-once)
Resource relocatesyour pointer danglesruntime updates the table; key unchangedsame — object holds the key, not the address
Refcounting / GCyour job, manuallyruntime can track it behind the keyobject lifetime can drive it (GC-managed)
Can enforce rulesnoonly what the API checksyes — the object curates what you may call
Costone memory accessa lookup, then the accessa method call, lookup, then the access
the trade You pay one indirection. In return the resource is free to move, be reference-counted, or be revoked — and your reference keeps working, or fails loudly instead of silently.

05 The JS wrapper · a handle wearing an object

A thin object around a native resource, exposing only a safe subset.

A bare handle can be a single integer. A JS wrapper is the same indirection dressed up as an object — so you can use normal syntax: properties, methods, await. The wrapper is the visible surface; the engine object behind it is the substance. Response, Request, and Headers are all wrappers — which is exactly why their bodies are read-once and some fields are immutable. The wrapper only exposes what the native object can safely let you do.

Below is a live Response. The amber surface is everything your JS can name; the teal substance behind the glass is the blink::Response and its one-time body stream. Poke the surface and watch which moves are allowed — and which the wrapper refuses on the native object's behalf.

the wrapper · visible surface JS

normal syntax — but a safe subset only

native substance · behind glass C++ · blink::Response

the real object & its one-time body stream

▸A fresh Response just arrived. Try reading a field, mutating a header, then draining the body — twice.
read it off the screen Every getter is a thin pass-through to a frozen native value. headers.set() is refused because a fetched response's headers are guarded. And the body drains once: the wrapper is the only handle to a stream that physically can't be rewound — so the second .json() throws.
wrapper vs handle A handle hides the pointer behind a key. A wrapper hides it behind an object — same membrane, friendlier surface, and a curated set of methods that decide what you're allowed to ask.

06 The [native code] tell

The handle looks like a value. Its behaviour is somewhere you can't read.

From your side a handle looks like an ordinary value or function. But ask the engine to show you the implementation and it can't — because there is no JavaScript to show. The body lives in C++, behind the boundary, reached through the same indirection. Run the expressions below.

node · the [native code] tell
the tell, in one line fetch.toString() reports [native code] because there is no JS body to print — the handle's behaviour lives in C++, reached through the boundary. typeof fd === "number" for the same reason: the kernel's file object isn't a thing JS can address, so you were handed a stand-in.

07 Same shape in JavaScript

The C indirection and the JS handle are the same picture.

On the native side the real thing is a pointer; it's stored in a private table and you get back an index — the handle, never the address. Later, your handle is resolved back to the pointer for you:

// Native side (C): the real thing is a pointer.
struct File* real = fopen("data.txt", "r");

// Stored in a private table; you get back an index
// into that table — the "handle". NOT the address.
int fd = table_insert(real);   // → 3

// Later, your handle is resolved back to the pointer:
struct File* p = table[fd];    // runtime does this, not you
fread(buf, 1, n, p);

The JavaScript is the same idea. fd is just a number standing in for a kernel file object you can't address directly — and fetch is a JS handle whose behaviour lives in C++, behind the boundary:

// fd is a number standing in for a kernel file object.
const fd = fs.openSync("data.txt");  // → 3
fs.readSync(fd, buf);                  // number crosses into the kernel

// fetch is the same idea: a JS handle whose
// behaviour lives in C++, behind the boundary.
typeof fetch         // "function" — but no JS body to read
fetch.toString()     // "function fetch() { [native code] }"
the symmetry table[fd] in C and the runtime resolving fd inside fs.readSync are the same lookup. You hold the index; the runtime holds the pointer.

08 Why this indirection exists

The extra hop, on purpose.

PropertyWhat the indirection buys you
Safetyyour code can't read freed memory or corrupt the heap — it never holds a real address
Relocationthe runtime can move or compact the resource and just update the table; your key is unchanged
Lifetimereference-counting or GC can run behind the key without your reference breaking
Revocationa stale key fails loudly (a clean error), where a dangling pointer fails silently (corruption)
Curated surfacewrapping the key in an object lets the runtime expose only a safe subset — and refuse the rest (read-once, immutable, closed)
Portabilitythe same handle or wrapper API survives the real implementation changing underneath
conclusion · three ways to hold one resource The resource really is a pointer, and you are simply not allowed to hold it. Hold it as a bare key and the runtime resolves it. Dress that key in an object and you have a wrapper — the same membrane, plus methods that decide what you may ask. Strip the membrane away and you have the raw pointer: one fast hop, and every bug this page exists to prevent.
Back to JIT-Atlas