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.
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.
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 pattern shows up everywhere a resource is owned by a lower layer. You always get a small reference, never the underlying memory:
| What you hold | The opaque key | The real thing underneath | Owner |
|---|---|---|---|
| File descriptor | a small int (e.g. 3) | kernel struct file* in the open-file table | OS kernel |
| GPU texture handle | an opaque ID / object | VRAM allocation + driver state | GPU driver |
| DB connection | a connection object | socket + session struct in the pool | driver / pool |
fetch() / DOM node | a JS wrapper object | C++ Blink object on the native heap | browser engine |
OS timer (setTimeout) | a numeric timer id | kernel/loop timer entry | event loop |
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
③ native memory C / C++
the real struct File* at an address
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.
p aimed at arr[0]. The teal cells are memory you legitimately own.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.
| Property | Raw pointer | Handle (key) | Wrapper (object) |
|---|---|---|---|
| What you hold | the actual address | an opaque key | an object wrapping the key |
| Indirection | none — one hop | one hop through a table | same hop — object resolves the key |
| Surface | raw memory, no API | a value; methods take the key | methods & properties on the object |
| Use after free | silent corruption / crash | key no longer resolves → clean error | object refuses → throws (closed / read-once) |
| Resource relocates | your pointer dangles | runtime updates the table; key unchanged | same — object holds the key, not the address |
| Refcounting / GC | your job, manually | runtime can track it behind the key | object lifetime can drive it (GC-managed) |
| Can enforce rules | no | only what the API checks | yes — the object curates what you may call |
| Cost | one memory access | a lookup, then the access | a method call, lookup, then the access |
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
Response just arrived. Try reading a field, mutating a header, then draining the body — twice.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.
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.
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] }"
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.
| Property | What the indirection buys you |
|---|---|
| Safety | your code can't read freed memory or corrupt the heap — it never holds a real address |
| Relocation | the runtime can move or compact the resource and just update the table; your key is unchanged |
| Lifetime | reference-counting or GC can run behind the key without your reference breaking |
| Revocation | a stale key fails loudly (a clean error), where a dangling pointer fails silently (corruption) |
| Curated surface | wrapping the key in an object lets the runtime expose only a safe subset — and refuse the rest (read-once, immutable, closed) |
| Portability | the same handle or wrapper API survives the real implementation changing underneath |