Embedding V8
Learn how programs host V8 with isolates, contexts, handles, native templates, and startup snapshots for fast JavaScript runtimes.
- 01Separate engine boundariesExplain the difference between an isolate's heap and a context's global JavaScript environment.
- 02Trace an embedder decisionDescribe why C++ code uses handles, scopes, templates, and explicit contexts around V8 values.
- 03Design a small runtime modelChoose which globals a host exposes and explain why a startup snapshot can shorten initialization.
A host puts V8 to work
V8 is a JavaScript engine. It parses and runs JavaScript, manages a heap, and provides the language built-ins. It is not a browser, a server, a timer system, or a network library. A program that uses V8 is called an embedder: Chrome, Node.js, and another C++ application can each put the engine inside a larger runtime.
Embedding V8 means a host application creates V8 engine instances, chooses JavaScript environments, and exposes selected native capabilities to code running in those environments.
The first question is simple: which globals did the host install? In a browser, timers and the document object normally exist. In Node, timers exist but a browser document does not. The language engine can run the same JavaScript syntax in both places; the surrounding host decides what that syntax can reach.
1console.log(typeof globalThis.setTimeout, typeof globalThis.document);Line 1 reads two names from globalThis and prints their types. A browser normally prints function object: setTimeout is callable and document is an object. In Node, the same code normally prints function undefined. That difference comes from the host, not from different JavaScript grammar.
Keep this boundary in mind as we move through V8's embedder API. An isolate is a whole engine heap. A context is one JavaScript world in that isolate. Handles are the native code's references to V8 values. Templates and snapshots let the host prepare useful worlds quickly.
Isolates: one engine heap
An isolate is one V8 virtual-machine instance with its own heap. Objects created in one isolate belong to that isolate; they are not ordinary JavaScript values in another isolate. An embedder can make more than one isolate when it needs separate engine state, such as separate workers or independently managed programs.
This is a memory boundary in V8, not a promise that all application security problems disappear. The host still chooses how to schedule isolates, what native objects they can call, and how messages cross between them. A worker is useful evidence: Node's worker threads run JavaScript in another V8 isolate with its own globalThis.
1import { Worker } from "node:worker_threads";2 3const worker = new Worker(4 "const { parentPort } = require('node:worker_threads'); parentPort.postMessage(globalThis === global);",5 { eval: true, execArgv: [] },6);7worker.once("message", (isOwnGlobal) => console.log(isOwnGlobal));Line 3 creates a Node worker. Line 5 asks the worker whether its own globalThis is its global object and sends the boolean back. The test runs this Node-only example and receives true. The result does not mean a worker shares the parent global; it means the worker has its own global environment.
Think of V8 as a game console processor. The device around it decides which buttons are wired up. One isolate is a whole console with its own memory card; a context is one game world running there.
- In real life: One console with its memory card
- In JavaScript: One isolate with its heap
- In real life: One game running on that console
- In JavaScript: One context with a global object
- In real life: Buttons wired by the device maker
- In JavaScript: Host APIs exposed by the embedder
- In real life: Another console
- In JavaScript: Another isolate
Where the analogy stops: A real V8 isolate has engine rules and memory management, not a physical game console.
This vocabulary makes the next two lessons easier to place. Agents and realms explains JavaScript's specification model, while realms and globals follows the observable global objects. Here we are looking below those concepts at an engine host.
Contexts: separate JavaScript worlds
A context is an execution environment inside an isolate. It has a global object and its own copies of built-ins such as Array. An embedder explicitly selects a context before compiling or running JavaScript, so unrelated code can run in separate worlds without silently sharing one global object.
Contexts are cheaper to explain with a small Node example. Node's node:vm module is not the full C++ embedder API, but it gives JavaScript a view of separate contexts. We make one context with a price global, then run a tiny expression inside it.
1const context = vm.createContext({ price: 10 });2console.log(vm.runInContext("price * 2", context));Line 1 creates a context whose global object starts with price: 10. Line 2 runs price * 2 in that context and prints 20. The expression is not looking up a variable in the file around it; it is looking up a global in the selected context.
A context is not just a bag of names. Its built-ins are separate too. That is why an array made in a fresh Node context fails an instanceof Array check against the outer context's Array constructor. The values have familiar behavior, but their constructors come from different JavaScript worlds.
Replay two contexts in one teaching isolate. Context separation is modeled with separate global records.
script
2const first = createContext(isolate, { price: 10 });3const second = createContext(isolate, { price: 3 });4console.log(run(first, "price * 2"));5console.log(run(second, "price * 2"));Node context evidence
It helps to separate a teaching model from a stable runtime observation. The model on this page keeps one object for each context's globals. Node gives real evidence with node:vm: a name placed in one context is not automatically present in another context.
1import vm from "node:vm";2 3const first = vm.createContext({ price: 10 });4const second = vm.createContext({});5console.log(vm.runInContext("price * 2", first));6console.log(vm.runInContext("typeof price", second));7console.log(vm.runInNewContext("[]") instanceof Array);Line 3 runs price * 2 in the first context and prints 20. Line 4 asks for price in the empty second context and prints undefined. Line 5 creates an array in a new context; its outer-context instanceof Array result is false because the constructors differ.
The test runs this source in Node, rather than pretending the browser sandbox can import node:vm. It proves separate globals and constructors. It does not claim that a context is an operating-system process or a complete security sandbox for hostile code.
Browsers commonly use contexts for windows and frames. A host can enter one context, run code, then exit and return to the earlier current context. The explicit selection prevents an embedder from accidentally running code against whichever global happened to be used last.
Handles and HandleScopes
JavaScript code sees objects directly. C++ code embedding V8 must use handles to refer to V8 objects. A garbage collector may move an object while compacting the heap, so V8 can update a handle to the object's new location. Native code should not keep a raw address and hope it remains correct.
A local handle lasts within a handle scope. A HandleScope groups local handles so native code can release the whole group together when that scope ends. Longer-lived native references use persistent-handle forms and need deliberate lifetime management.
1// Simplified sketch of the embedder API; C++, not runnable JavaScript.2v8::HandleScope scope(isolate);3v8::Local<v8::String> name = v8::String::NewFromUtf8Literal(isolate, "Asha");4// When scope ends, these local handles are released together.Line 2 opens a scope for local handles. Line 3 creates a local handle for a V8 string. Line 4 states the key lifetime rule: leaving the scope releases its local handles together, and an object may then be collectible if nothing else reaches it.
Imagine taking a tray at a buffet. While you hold it, the plates on it stay with you. Return the tray once, and all its plates go back together; a HandleScope similarly releases its local handles at one boundary.
- In real life: A tray carries your plates
- In JavaScript: A HandleScope owns local handles
- In real life: A plate stays with your tray
- In JavaScript: A handle keeps a V8 value referenced
- In real life: Return the tray once
- In JavaScript: Close the scope and release local handles
- In real life: Keep one plate for later
- In JavaScript: Use a longer-lived persistent handle carefully
Where the analogy stops: Handles are GC-aware references, not physical plates, and persistent lifetimes need explicit API rules.
This matters because handles bridge native and managed memory. JavaScript references keep JavaScript objects reachable. Handles let C++ do the same while it is working. The V8 guide also warns that a local handle cannot be returned from a closing scope without an escape mechanism.
Templates expose native functions
A FunctionTemplate is a V8 blueprint for a JavaScript function backed by a C++ callback. The embedder can put that function on a context's global object. When JavaScript calls it, V8 transfers control to the native callback with V8 values and a defined calling convention.
You can first model the idea with ordinary JavaScript. This example makes a fresh object that represents a new global object, installs one function, then calls it. It is not native code; it makes the host's choice visible before the C++ sketch.
1const globalObject = {};2 3globalObject.double = (price) => price * 2;4console.log(globalObject.double(10));Line 1 creates a fresh global-shaped object. Line 3 installs double on that object. Line 4 calls it with 10 and prints 20. A real embedder uses V8 APIs and a native callback, but the decision is the same: no installation means no function.
1// Simplified sketch of the embedder API; C++, not runnable JavaScript.2auto global = v8::ObjectTemplate::New(isolate);3global->Set(v8::String::NewFromUtf8Literal(isolate, "double"),4 v8::FunctionTemplate::New(isolate, DoubleCallback));5auto context = v8::Context::New(isolate, nullptr, global);Line 2 makes a global-object template. Lines 3 and 4 associate the JavaScript name double with a native callback blueprint. Line 5 creates a context using that global template. Browser DOM bindings and Node built-ins are larger versions of this host-to-JavaScript bridge.
Build a tiny host
The next playground is deliberately small. It creates an isolate model, creates a context with price: 10, and lets you toggle which host functions are installed. The runner accepts only numbers, names, arithmetic, parentheses, and calls to installed functions. It does not use eval or new Function.
Start with the visible expression. Toggle one host function, run the same expression, and watch the result or the precise missing-function message change. Reset rebuilds the isolate and context from their initial state rather than only clearing the output panel.
1const isolate = createIsolate();2const context = createContext(isolate, { price: 10 }, { double });3const result = run(context, "double(price)");4console.log(result);3Globals: price = 10
doubleOnly these host calls are available.
20The safe runner evaluated the selected expression.
The selected context has price: 10. Its result is 20.
This is a teaching model, not an implementation of V8 parsing, handles, or security checks. Its narrow language is intentional: it lets the page demonstrate the host boundary safely. A real embedder gives V8 JavaScript source and manages errors through the V8 API.
Snapshots for embedders
A startup snapshot is prepared V8 heap state used during initialization. New contexts need built-ins and a global object ready to use. Instead of rebuilding all of that work from scratch, V8 can deserialize prepared heap data, making startup faster.
V8's custom-startup-snapshot documentation describes capturing initialized heap state, then configuring a new isolate to use it. The snapshot is not a general live-state backup. It cannot safely capture every interaction outside V8's heap, and values like a captured current time would be frozen at snapshot creation.
1// Simplified sketch of the embedder API; C++, not runnable JavaScript.2v8::Isolate::CreateParams params;3params.snapshot_blob = custom_snapshot_blob;4auto* isolate = v8::Isolate::New(params);Line 2 prepares isolate creation options. Line 3 supplies prepared snapshot data. Line 4 creates an isolate using those options. The exact production setup has more ownership and initialization details; this short sketch only shows where the snapshot enters the embedder API.
Node and Chrome use startup snapshots. For the user-facing reason, connect this to startup performance: doing repeatable setup before a program begins can reduce work on the critical path. A runtime author must also keep snapshot data deterministic and compatible with the build that reads it.
What runtime authors decide
Application developers rarely call the V8 C++ API, but runtime authors make choices that reach every application developer. They choose which globals exist, how host callbacks report errors, how many isolates to create, when to create contexts, and which startup data is safe to prepare ahead of time.
The same engine can therefore feel different in a browser, Node, an edge runtime, or a test harness. JavaScript's core syntax travels with the engine; platform features travel with the host. Before assuming an API exists, identify which runtime is running your code.
- Owns a V8 heap for one VM instance.
- Owns a global object and separate built-ins.
- Decides whether
setTimeoutexists. - Keeps a V8 object referenced while C++ is using it.
- Can hold
price: 10without changing another global world. - Chooses prepared startup data for new initialization.
Sort each card by the layer that owns the behavior. Read the explanation after each choice.
When you design a library, this is practical knowledge too. Avoid quietly assuming a browser document in code meant for Node. Keep host dependencies at the edge of your program, and write tests that name the runtime behavior you need.
Common misconceptions
- “V8 is the browser.” V8 is an engine. The browser adds DOM, fetch, rendering, and event-loop behavior around it.
- “Every context is a separate isolate.” One isolate can hold multiple contexts with separate globals.
- “A context makes untrusted code safe by itself.” Security needs a complete host policy, not only a new global object.
- “A handle is a JavaScript variable.” A handle is a native-side reference used through the V8 API.
- “A snapshot records any live application state.” It captures permitted prepared heap state, not arbitrary outside resources.
| Term | What it is | Not the same as |
|---|---|---|
| V8 | The JavaScript engine that executes language code. | The complete browser or Node.js runtime. |
| Host API | A capability supplied around V8, such as timers or DOM APIs. | A JavaScript built-in required by the ECMAScript language. |
| Context separation | Separate globals and built-ins within one isolate. | A security boundary sufficient for untrusted code by itself. |
| Snapshot | Prepared heap state for faster initialization. | A live heap inspector or a universal serialization format. |
A useful habit is to name the layer before explaining a behavior: JavaScript language, V8 engine, embedder API, or host platform. That one question prevents many fuzzy claims about globals, isolation, and startup.
Practice exercises
Predict the browser-shaped output. Then say which layer decides that document exists.
console.log(typeof globalThis.setTimeout, typeof globalThis.document);A browser normally prints function object. Node normally has timers too, but no browser document, so its second word is undefined.
Type the two outputs in order, separated by a comma.
const first = { price: 10 };
const second = {};
console.log(first.price * 2);
console.log(typeof second.price);The program prints 20 and then undefined. Separate context globals work the same way: a name in one global object is not automatically in another.
Which function should a tiny price-calculating host install on its global object?
const globalObject = {};
globalObject.double = (price) => price * 2;
console.log(globalObject.double(10));Install double. A host exposes capabilities by choosing a JavaScript name and connecting it to its implementation.
When a HandleScope ends, what group of references is released together?
Local handles close together when a HandleScope ends. That releases the scope's native references in one place.
You are building a small test runtime for pricing rules. What should you decide before you run a rule?
Decide which host APIs or globals to expose before code runs. That decision defines what scripts can ask the surrounding program to do.
For a runtime that starts often, what repeated work can a startup snapshot avoid?
A startup snapshot avoids repeating initialization, especially setting up prepared built-ins and permitted heap state.
Check your understanding
For each question, identify whether it asks about the engine, a context, native embedding code, or a host decision.
Question 1 of 7Who decides whether
documentexists onglobalThis?Choose an answer to see the explanation.
Question 2 of 7What does this print?
Read the code, then predictPop out in the code editor (opens in a new tab)JavaScriptconst first = { price: 10 }; const second = {}; console.log(first.price * 2, typeof second.price);Choose an answer to see the explanation.
Question 3 of 7Which statement describes an isolate?
Choose an answer to see the explanation.
Question 4 of 7Why does native C++ use a local handle?
Choose an answer to see the explanation.
Question 5 of 7What does a FunctionTemplate describe?
Choose an answer to see the explanation.
Question 6 of 7What is a startup snapshot for?
Choose an answer to see the explanation.
Question 7 of 7Which Node feature gives JavaScript evidence of separate globals?
Choose an answer to see the explanation.
Key takeaways
- V8 executes JavaScript; the host decides which APIs and globals surround it.
- An isolate is a V8 VM instance with its own heap, while a context is one global JavaScript environment inside it.
- Handles let C++ refer safely to V8 values, and HandleScopes release local handles together.
- Templates are blueprints that let an embedder expose native functions and objects to JavaScript.
- Startup snapshots prepare allowed heap state so initialization can do less repeat work.
Remember the one-liner.
An embedder turns V8 into a runtime by choosing its worlds, capabilities, and startup state.
Coming next: Node.js architecture, where V8, libuv, and C++ bindings become one practical server runtime.