Web Platform · JavaScript Runtime
Realms, Cross-Realm Identity, and Errors That Cross Boundaries
Each global object carries its own set of intrinsics, so objects from an iframe or worker fail instanceof checks while remaining fully usable and holding their realm alive.
Every global object comes with a full set of built-in constructors and prototypes created alongside it.
An iframe’s Array is not the parent’s Array; its Object.prototype is a different object; its
Promise is a different function with a different prototype. The specification calls this bundle a
realm, and makes it the unit of intrinsic identity. Code that assumes one set of built-ins per process,
or even per tab, breaks in ways that look like type errors but are really identity errors.
The distinction between realm and thread matters here. A thread runs one event loop; a document and its
same-origin iframes typically share that loop, so their tasks and microtasks interleave in a single
order. Each document, however, has its own realm. Objects can therefore cross the boundary without any
serialization — passing a reference through window.parent.someFunction hands over the same object —
while the constructors that produced them remain distinct.
Two sets of intrinsics in one thread
A realm record holds, among other things, the global environment, the global object, and the intrinsic
objects: Object, Function, Array, Promise, Error and the rest, plus the prototypes they install.
Initialization happens once, when the realm is created, so the functions are shared by everything running
in that realm but by nothing outside it.
Within one thread, several realms can exist simultaneously. Each iframe creates one. A dynamically
inserted iframe that is later removed keeps its realm alive as long as anything reachable from the parent
still references an object created there. A MessagePort to a worker connects two realms on different
threads, with the boundary described in worker messages; a postMessage to a same-origin window usually connects two realms on the same thread. The scheduling
consequences differ — separate threads have separate loops — while the identity consequences are the same.
Because the intrinsics differ, so do the prototype chains. An array created by the iframe’s Array has
the iframe’s Array.prototype in its chain, which has the iframe’s Object.prototype at its end, and
neither object is the parent’s. The array is still an array in every way that matters to the engine: it
has the internal slot that array methods check.
Identity versus usability
Passing an object across a same-origin frame boundary preserves identity. parentObj === childObj is
true when the child was given the parent’s object. What fails is the classification performed by
instanceof, which reads the prototype chain and compares against a constructor’s prototype property.
The chain contains the child’s Array.prototype, not the parent’s, so:
// In the parent document
const foreign = iframe.contentWindow.eval('[1, 2, 3]');
foreign instanceof Array; // false — different Array.prototype
Array.isArray(foreign); // true — checks the internal slot
foreign.constructor === Array; // false
Object.prototype.toString.call(foreign); // '[object Array]'
Array.isArray is specified to work across realms precisely because it tests an internal property rather
than a prototype. Object.prototype.toString works because it inspects an internal slot for the built-in
tag. typeof works, since it does not consult prototypes. JSON.stringify works, since it walks own
properties and does not care which realm’s Object they belong to.
The general lesson: any check that depends on a constructor identity is a realm-local check. x instanceof Map, x instanceof Date, x instanceof Error, x instanceof Promise — all of them are false for an
object built by another realm’s constructor, and all of them are true only when the two objects happen to
come from the same realm. Duck typing by shape, or brand checks through internal-slot-testing methods,
survive.
Brand checks and their limits
Some built-in methods are generic: they accept any object with the right internal slots and operate on
it regardless of prototype. Array.prototype.map, Array.prototype.slice, and the rest of the array
methods are generic in this sense. Others are specified to require the right internal slot and throw a
TypeError otherwise: Map.prototype.get on a non-Map, Date.prototype.getTime on a non-Date,
RegExp.prototype.exec on a non-RegExp. These checks are what make it possible to validate a value
without trusting its prototype chain.
The pattern Object.prototype.toString.call(x) returns a stable tag for built-in types and is a common
cross-realm substitute for typeof on exotic objects; it is not reliable for user classes, which report
[object Object] unless they define Symbol.toStringTag. That symbol is a second route to cross-realm
classification if the sending side sets it, but it is user-controlled and therefore advisory rather
than binding.
Symbol.hasInstance adds a hazard. A class can define a static [Symbol.hasInstance] that returns
whatever it likes, and a library that patches instanceof this way changes the check for every consumer
of that class, including code in other realms that happens to use the same constructor. Since the
mechanism is invoked by the operator itself, no brand check on the object can undo it.
Errors are the worst case in practice
Error objects are the place where cross-realm identity produces the most confusing failures. An error
thrown by code in an iframe and caught in the parent is a genuine object with a message, a stack, and
a prototype chain ending in the iframe’s Error.prototype. err instanceof Error is false, so the
conventional guard if (err instanceof Error) rejects a value that is, in every operational sense, an
error.
The reliable test is structural: check that the value is an object, that typeof err.message === 'string',
and that typeof err.name === 'string'. That treats subclasses, DOMException, and cross-realm errors
alike. The name property is the only stable classifier for the built-in error types after a realm
crossing, and it is writable, so it is a hint rather than a proof.
Cross-origin frames add a security layer on top. The error event delivered to the parent’s global
handler for an exception in a cross-origin child carries the generic message “Script error.” with empty
source and line information, because the details would leak information about another origin. The event
still fires; the diagnostic content is suppressed. Structured error reports have to be arranged
explicitly through postMessage, which the child’s own code must perform, since the browser will not
describe the failure on its behalf.
Errors that cross a worker boundary do not travel as objects unless something sends them: an uncaught
error in a worker is reported through an ErrorEvent whose message, filename, lineno and colno
describe the failure. A postMessage that sends an actual Error uses structured serialization, which
reconstructs an Error in the receiver’s realm with message and stack preserved, and with the
receiver’s Error.prototype — so the same instanceof problem arises, but the object at least has the
right internal slots.
Lifetimes: a detached frame is still a realm
Removing an iframe from the document detaches its browsing context but does not tear down its realm immediately. Any JavaScript reference to an object created inside it keeps the entire realm reachable, which means keeping its global, its intrinsic graph, and every document node it references alive. A listener registered on the parent’s global object that closes over a variable from the child is enough to retain the child’s document after removal. This is the mechanism behind the familiar detached-DOM memory growth in applications that create and discard frames.
The same holds in the other direction with workers. The Worker object itself is a parent-realm object,
and a handler assigned to its message event closes over parent state that stays reachable for as long
as the worker reference does; the worker’s own realm does nothing to weaken that. A transferred port
received on the parent side behaves the same way. The lifetime rule is uniform: reachability across
realms is ordinary reachability, and the boundary does not weaken it.
There is no API to enumerate realms or to ask which realm an object belongs to. window.frames lists the
child browsing contexts, and a worker object gives access to its postMessage and termination, but the
intrinsics are only reachable through the global object a reference points at. Code that needs to handle
objects from multiple realms must therefore test structurally, avoid constructor comparisons, and be
explicit about which realm’s constructors it uses when it creates values that will be handed back.
Promises and the scheduling question
A promise created in an iframe and awaited in the parent works, because await only requires a thenable
and the promise’s own reactions are queued as jobs on the thread’s job queue, not on a per-realm queue.
The value passed to the parent’s continuation is the iframe’s value, with the iframe’s prototypes, which
circles back to the identity problem for whatever was resolved.
When the two realms are on different threads, ordering is not shared. A promise settled in a worker and a
promise settled in the page have no relative order beyond what message delivery imposes, and a
Promise.all over both resolves in whichever order the jobs happen to run. Nothing in the specification
orders jobs across threads, so any protocol that depends on it has to impose the ordering itself through
explicit sequencing.
References
- ECMAScript Language Specification — Realms — Ecma International
- Array.isArray() — MDN Web Docs — MDN Web Docs
- instanceof — MDN Web Docs — MDN Web Docs