Runtime deviations
Ahead of user code the sandbox installs the standard library, and that library targets ECMA-429, Minimum common web API. §5 puts every API in scope, and it ends by saying what a runtime owes when it cannot follow one: “Where any runtime environment must diverge from a normative requirement for technical or structural reasons, clear documentation shall be provided. Documentation shall include both explanation and impact of deviation.” This page is that documentation. A divergence with no row here is a defect, not a feature.
Two structural facts account for most of what follows. The isolate performs no I/O of its own, and the host that runs every HTTP exchange hands the whole answer back at once, so nothing inside the isolate ever holds a partial body. It is also one realm with no workers, no document and no origin, so a browser concept a specification is written against sometimes has no counterpart here.
An absent name comes in two forms. Most are absent completely: reading one answers undefined, not a stand-in that fails later. But a few are declared absences, where the executor protocol publishes the name and rejects its first touch with a text naming the reason and what remains possible. A published name answers typeof as if it were served, so code that detects capabilities by typeof meets the rejection inside its guarded branch. Every such wording is wire contract and lives in the declared absences. The rows below say only which form an absence takes.
One declared absence has no row here. ECMA-429 asks for no cache storage, so publishing caches and refusing every call is no divergence at all. The persistence contract declares that refusal and explains it.
The map of the runtime’s modules is in Architecture. Where a row states a bound on what the guest can reach, the caps on the crypto bridge among them, the threat model says which boundary that bound serves.
Interfaces ECMA-429 requires that this runtime does not carry
Section titled “Interfaces ECMA-429 requires that this runtime does not carry”| Absent | Requirement | Reason | Impact |
|---|---|---|---|
CompressionStream, DecompressionStream |
ECMA-429 §5.1, by way of the Compression Standard | The host performs the HTTP exchange and handles content coding there, so nothing inside the isolate ever compresses anything. | Both constructors exist and throw, as a declared absence. Content coding on the wire still happens, because the host applies it, so a program that never meant to compress by hand notices nothing. |
MessageChannel, MessagePort |
ECMA-429 §5.1, by way of HTML §9.4.2 | One isolate is one realm with no workers, so there is no second endpoint for a port to reach. A channel in a single realm is left with macrotask scheduling alone, and the timers already serve that. | Code that opens a channel fails at the missing constructor, the earliest point a program can learn. A channel that existed and never delivered would surface later and read as a hang. |
The whole WebAssembly namespace |
ECMA-429 §5.1 and §5.2, by way of the WebAssembly JavaScript Interface | A WebAssembly memory is allocated where the isolate’s memory limit cannot meter it, and no arrangement inside the isolate brings it back under the meter. | typeof WebAssembly reads undefined, so code that feature-detects takes the branch it already has. Code that assumes the namespace fails at the first reference. |
Blob.prototype.textStream, Body.textStream |
File API §3.3.6, Fetch §5.3 | Both members reached their standards long after the interfaces they hang on, and no property of the isolate stands behind their absence. | Code written against a runtime that does carry them, Node among them, fails at the missing method. stream().pipeThrough(new TextDecoderStream()) reads the same body with what is carried. |
WebAssembly is one of three places where V8 hands an isolate memory the embedder’s allocator never sees. Two more paths exist: the growth of a resizable ArrayBuffer and the growth of a growable SharedArrayBuffer, both ECMA-262 members and not anything ECMA-429 lists. Each path is measured. A 128 MiB isolate holds gigabytes through any of the three. This product rests on the bound the engine enforces on the metered heap, so all three paths are closed.
But the two growth operations are closed differently from WebAssembly. ArrayBuffer.prototype.resize and SharedArrayBuffer.prototype.grow stay and throw, because no code feature-detects a method before calling it and a missing method would say only that a method is missing. Each throw names the reason and tells the guest what works instead: asking for the length up front. A buffer created at the length it needs is metered like anything else, and ArrayBuffer.prototype.transfer is untouched for the same reason. All three closures happen in the handshake and not in the snapshot. An isolate writing a startup snapshot has neither WebAssembly nor SharedArrayBuffer, and V8 installs both again on every isolate it restores. ArrayBuffer.prototype.resize is there while the snapshot is written and a replacement of it survives the restore, and it closes in the handshake with the other two. The threat model states which boundary this serves. The memory family of the benchmark proves each path twice over: by what the guest is answered, and by the memory of the process while it asks.
Behaviour that diverges inside the interfaces this runtime carries
Section titled “Behaviour that diverges inside the interfaces this runtime carries”| Requirement | Deviation | Reason | Impact |
|---|---|---|---|
| Fetch §2.2.4: a body is a stream, so it hands out the chunks the wire delivered as they arrive. | A response body is one chunk carrying the whole answer, and a request body whose source is a stream is read to its end before the exchange begins. | The host performs the whole exchange and hands the answer back at once, so no partial body exists inside the isolate to hand out. | Reading a response body incrementally works and answers everything in one read, so a progress bar driven by it jumps from nothing to done. A request body that never ends never reaches the host. |
Fetch §3.2: a request carries an Origin header derived from its client’s origin, on every CORS fetch and on every request whose method is neither GET nor HEAD. |
No Origin header is added. |
The isolate has no origin. ECMA-429 §5 names this divergence itself, as the example a server runtime cannot avoid. | A server that authorizes on Origin sees none. The host performs the exchange and may add one of its own. |
Fetch §2.2.6: a response carries a type, one of basic, cors, default, error, opaque or opaqueredirect, and its URL points at the last one it landed on. Fetch §5.5 reads redirected off the length of that URL list. |
type is default for every response the host answers with, redirected is always false, and url is the URL the request named. |
The host follows the redirect chain, so the isolate sees one answer and never the chain that produced it. | Code that branches on type or on redirected takes the same branch every time, and code that reads url to learn where it landed reads where it aimed. |
Fetch §2.2.5: a request’s redirect mode decides whether a redirect is followed, and error makes the fetch reject. |
redirect is validated, stored and answered by the getter, then not carried outward. Whoever performs the request decides, and every outbound path this runtime ships follows redirects. |
The outbound wire record is the method, the URL, the headers and the body. Redirect handling belongs to whoever performs the exchange, and neither the native path nor a host function is told what the guest asked for. | redirect: 'error' and redirect: 'manual' do not do what they say. A request to an endpoint that answers 302 is followed, and the guest receives the body of a URL it never named, with redirected false and url still the original. Redirect mode is not a boundary here. A host that needs one enforces it in its own outbound function, the only place a redirect can still be refused. |
| Fetch §4.1: a request’s integrity metadata is checked against the response body, and a mismatch fails the fetch. | integrity is validated, stored and answered by the getter, and no check runs. |
The check would have to run where the body arrives, on the host, and the metadata does not cross with the request. | A subresource integrity string has no effect. A body that does not match the digest is delivered as if it did. Code that needs the check digests the body it received with crypto.subtle.digest and compares against the same value it put in integrity. |
Fetch §5.4: RequestInit declares mode, credentials, cache, keepalive, referrer and referrerPolicy. |
All six are validated, stored and answered by the getter, and none reaches the exchange. | Each governs a browser’s own connection, its cookie jar, its HTTP cache or the referrer it discloses. The isolate has none of them, and the host owns the connection. | Code that sets them observes them on the request object and sees no effect on what is sent. A host that needs any of them applies it in its own outbound function. |
Fetch §5.4: RequestInit also declares duplex, priority and window, and the constructor throws a TypeError on a non-null window. |
The three members are never read out of the init object, so a getter defined on one of them never runs and a non-null window is accepted in silence. |
duplex describes a streaming body the host would have to buffer anyway, and priority reaches a scheduler the isolate does not have. Neither changes an answer here. |
One shape differs visibly: new Request(url, { window: 1 }) is built here and rejected in a browser. Request.duplex still answers half, which Fetch defines it to answer whatever the init said, and Fetch puts no getter for the other two on Request at all. |
Fetch §5.1: a Headers object drops a forbidden request-header name and a forbidden response-header name behind its guard. |
Every name that passes the token production is accepted. | The forbidden lists guard a browser’s own connection. The host owns the connection here and applies its own policy. | new Headers({ Host: 'a.example' }) keeps the header, and so does Connection. Whatever an outbound function forwards reaches the wire, so sanitizing is the host’s job. |
Fetch §5.3: blob() answers a Blob whose type is the parsed MIME type, serialized. |
The type is the raw Content-Type value, ASCII-lowercased by Blob and emptied when it carries anything unprintable. |
The runtime carries no MIME type parser, and every branch that needs one reads the essence instead. | A response typed text/plain; charset="utf-8" answers a Blob whose type still carries the double space and the quotes, where a browser answers text/plain;charset=utf-8. |
| HTML §2.7.3 dispatches on the internal slots a value carries and rejects an exotic object it writes no steps for, naming a proxy as the example. | A live proxy is cloned as the object its traps describe, and a revoked proxy is rejected. | Asking a value what it is means invoking it, and a proxy answers through its traps. The rejection falls out of that: reading the class string of a revoked proxy throws, and the clone reports A Proxy could not be cloned. from where the throw lands. |
structuredClone of a proxy answers the trapped view and runs the traps while it walks, so a getter that lies produces a clone that holds the lie. |
| HTML §2.7.3 reads the internal slots, which no prototype can move. | The clone dispatches on the class string instead. Most exotic objects still answer that from a slot, but Map, Set, ArrayBuffer, SharedArrayBuffer and a boxed BigInt answer it from Symbol.toStringTag on their prototype. |
Recognizing a re-pointed exotic object means probing every intrinsic and catching a TypeError for each miss, on every ordinary object cloned, and the clone itself costs far less than that. |
Pointing a Map at Object.prototype makes structuredClone answer a plain object where a browser answers a Map, and pointing it at a prototype that lends it a class string of its own, Promise.prototype for one, makes the clone fail with a DataCloneError. A Date, a RegExp or an Error treated the same way still clones correctly. None of these shapes appears in code that clones data. |
HTML §2.7.3 clones a SharedArrayBuffer as a second object over the same data block. |
A SharedArrayBuffer is cloned as itself. |
One isolate is one agent cluster with one realm, so there is no second wrapper for a shared buffer to become. | structuredClone(sab) === sab here, where a browser answers a distinct object over the same memory. Since the buffer cannot grow in this runtime, no view over it can observe the difference. |
Web Cryptography §13: a CryptoKey is an opaque reference to keying material the user agent manages, and no clause bounds how many keys an application may hold or how many operations may run at once. |
The material stays in the host process and the isolate holds the number the key answers to. An execution may create keys up to KEY_LIMIT in packages/executor/src/sandbox/crypto.ts, and one operation runs at a time. |
Material inside the isolate would be readable by the code the isolate runs, whether the key was imported as extractable or not. Host work sits outside the memory limit and the timeout of the isolate, so the count of keys and the number of threads an execution occupies are capped where those limits cannot reach. | A CryptoKey behaves as the standard describes, exporting a non-extractable key included. A loop that generates keys meets the rejection instead of growing the host, and concurrent operations answer in the order they were asked for. An operation still queued when the execution ends never starts, while one already running finishes, so a derivation that states a huge iteration count is paid for once. |
| Web Cryptography §18.4 normalizes an algorithm by reading the members its dictionary declares, which Web IDL reads through the prototype chain. | Only the members an algorithm object owns are carried to the host, and only to a fixed depth, one level deeper than the deepest member any dictionary of the interface declares. | Normalization happens on the host, where the algorithm tables already are, so the data the object holds crosses, and not a dictionary this runtime would have to declare a second time. | crypto.subtle.digest(Object.create({ name: 'SHA-256' }), data) rejects with the TypeError of a missing name where a browser reads the inherited member and hashes. Every algorithm written as an object literal is unaffected, and that is how they are written. |
High Resolution Time §2.1: performance.now reads a monotonic clock, which system clock adjustments cannot move. |
The reading comes from Date.now and is held against the highest value already handed out. |
A bare V8 isolate exposes no monotonic clock, and holding the value keeps two chronologically recorded readings from ever differing negatively. | A measurement shorter than a millisecond reads as zero, which the standard permits, since it lets an implementation coarsen the clock further than the 100 microseconds it names. A backwards adjustment of the system clock stalls the reading instead of rewinding it. Event.timeStamp shares the clock and behaves the same way. |
ECMA-262 §27.5.1.9 defines HostPromiseRejectionTracker, and HTML §8.1.6.4 wires it to the unhandledrejection and rejectionhandled events. |
Neither event ever fires, though both handler attributes exist and the interfaces are carried, because ECMA-429 requires them. | Nothing in a bare isolate reaches the host hook that would fire them. | A listener on either name never runs. A rejection nobody handled is dropped and not reported, so it reaches neither the program’s own handler nor the caller’s logs. A rejection the entry function awaits or returns still reports as the execution’s error, so code that must not lose one keeps its promises on that path or attaches a catch where each is created. |
Unicode and encoding tables this runtime does not carry
Section titled “Unicode and encoding tables this runtime does not carry”Two standards ask for a large Unicode table at a point a server runtime rarely stands on. Both tables stay out. Each row says what stands in their place and what a program gives up for it.
| Requirement | Deviation | Reason | Impact |
|---|---|---|---|
URL §3.3: the domain parser runs Unicode ToASCII with CheckBidi and CheckJoiners set, which maps every code point through the UTS46 table and applies the bidi and joiner rules. |
A domain that is ASCII takes URL §3.3’s own ASCII path, written in the standard as lowercase-and-return and touching no table. A domain that is not ASCII runs the four steps of UTS46 §4 against Unicode 17.0.0 and meets every criterion of §4.1 but two: the ContextJ rules of RFC 5892 appendix A and the bidi rule of RFC 5893 §2 do not run. | UTS46 §6 derives the mapping table from NFKC_Casefold, which the engine already carries, so the runtime ships the difference and not the table. The two skipped checks read the Joining_Type and Bidi_Class properties, and the engine hands neither out: \p{General_Category=Mark} compiles while \p{Bidi_Class=Arabic_Letter} and \p{Joining_Type=Dual_Joining} are both syntax errors. |
A label that mixes an Arabic letter with a Latin one is encoded instead of refused: new URL('https://ا1-a.example/').hostname answers xn--1-a-nze.example here, where an ICU-backed parser rejects the URL outright. The name that comes out does not exist, so the request fails at DNS and not at the URL parser. |
Encoding §4.2: the table names every encoding and the labels each answers to, and §7.2 has TextDecoder throw a RangeError for a label naming no encoding it can decode. |
UTF-8, UTF-16BE, UTF-16LE and windows-1252 are carried, under every label the index gives them. Every other label is rejected with that RangeError. |
The rest of the index is legacy multi-byte mapping tables, and no byte sequence inside this isolate arrives in one. The four carried encodings are algorithms and not tables, apart from the short one windows-1252 needs. A response without a usable charset falls back to windows-1252. | new TextDecoder('shift_jis') throws RangeError: Failed to construct 'TextDecoder': The encoding label provided ('shift_jis') is invalid. Code that has to read a legacy encoding decodes it itself. A label typo is caught here, not silently treated as UTF-8. |
Shapes that only look like divergences
Section titled “Shapes that only look like divergences”URLPattern compiles every component with the regular expression v flag, which URL Pattern §1.5 requires. Inside a character class that flag makes / a syntax error, so a component pattern spelled ([^/]+) raises and ([^\/]+) is the spelling that works. Chrome answers the same way. Code carried over from urlpattern-polyfill, which compiled with u and accepted both, has to change that one spelling and no more.
queueMicrotask is built on the promise job queue, because awaiting an already resolved promise is the only microtask queue a bare isolate offers. That queue is the same one HTML §8.8 enqueues onto, and the await resumes in the job the callback would have occupied, so the callback lands where a browser puts it. Interleaving queueMicrotask with then callbacks produces the same order here as it does under a native implementation, measured against Node.
performance carries now, timeOrigin and toJSON and no other member. Those three are exactly the Performance interface High Resolution Time §7 defines, and exactly what ECMA-429 requires. performance.mark, performance.measure and performance.getEntries belong to the Performance Timeline. ECMA-429 names that only as an extension a runtime may add, so a library that records its own timings fails at the call without anything being out of conformance.
navigator carries userAgent alone, valued Supolka-Executor. ECMA-429 §5.2 requires that member and asks for no other on the object, and §7 recommends a single product token with no version. So navigator.language and navigator.hardwareConcurrency read undefined by the standard’s own design and not against it.
Serializing application/x-www-form-urlencoded takes an encoding in the URL Standard, and this runtime always uses UTF-8, the default the standard itself states. HTML form submission is the only caller that passes anything else, and this runtime does not have it. URLSearchParams is UTF-8 in every direction on every conforming runtime.
ECMA-429 §6 permits a conforming runtime to omit the readonly attribute for the properties it adds to the global scope, so every global this runtime publishes can be overwritten and deleted on purpose.
DOM §2.9 walks an event path, and with no node tree the path holds the target alone. Both passes therefore run at the target, eventPhase reads AT_TARGET throughout, and composedPath answers the target alone. For a lone EventTarget the algorithm produces exactly that, and none of it is a simplification.