Bindings: how DOM objects exist in JavaScript
Learn how Web IDL turns browser objects into JavaScript wrappers with conversions, prototypes, live collections, DOMException, and legacy quirks.
- 01Read bindingsExplain how Web IDL exposes browser capabilities to JavaScript.
- 02Predict conversionDistinguish DOMString, USVString, and sequence conversion.
- 03Debug platform objectsRecognize wrappers, prototypes, live collections, and legacy exceptions.
A browser API is a binding
JavaScript can call document.createElement, read document.body, and build a URL. Those values are not invented by JavaScript alone. The browser exposes its platform objects to JavaScript through a binding layer.
A binding is the agreement that makes a browser capability appear as a JavaScript value, property, method, constructor, or exception. It includes argument conversion, object identity, prototypes, and unusual old behavior kept for compatible websites.
Web IDL is the language web specifications use to describe an API surface and how that surface maps into JavaScript. The browser implementation may use C++, Rust, or another language behind that surface.
This follows How input reaches your event handlers. Input handlers are JavaScript functions, but their event objects, targets, and document methods arrive through these bindings. Next, Realms, globals & WindowProxy explains why each global gets its own related objects.
Web IDL is the printed manual
Think of a TV remote. You press a button, but the electronics inside the TV do the work. The printed manual says what the button accepts and what it does. JavaScript is the remote; browser implementation objects are the electronics; Web IDL is the manual.
You can change the channel without seeing the circuits. The remote still has strict buttons and rules, so the TV knows what each request means.
- In real life: A remote button
- In JavaScript: A JavaScript method call
- In real life: Electronics inside the TV
- In JavaScript: Browser implementation objects
- In real life: The printed manual
- In JavaScript: A Web IDL interface definition
- In real life: A button accepts one kind of input
- In JavaScript: A binding converts and validates arguments
Where the analogy stops: A browser API can do much more than a remote button. The analogy only explains the boundary between JavaScript and implementation code.
An IDL interface names attributes and operations. In JavaScript, regular operations usually become methods on an interface prototype, while an interface object is usually available on the global object. The specification also states which globals expose it.
This is why API behavior is more exact than a TypeScript declaration alone. Web IDL specifies conversion before an operation runs. If conversion fails, the operation does not run. That detail explains many useful browser edge cases.
// Simplified Web IDL, not the whole DOM Standard.
interface Element {
undefined setAttribute(DOMString name, DOMString value);
};
const box = document.createElement("div");
box.setAttribute("data-count", 5);
console.log(box.getAttribute("data-count"));The first three lines are a deliberately simplified reading aid, not a copy of the DOM Standard. interface Element names the kind of platform object. undefined says setAttribute does its work without returning a useful value.
The two DOMString words describe conversion at the boundary. In the JavaScript call, the number 5 reaches the second parameter and becomes the string "5". The final log prints 5, because the browser stored the converted string as the attribute value.
Read IDL as a compact contract: interface name, member name, return type, then argument types. It describes what JavaScript can observe. It does not promise a particular engine class, memory address, or internal implementation language.
DOMString, USVString, and sequences
Web APIs do not merely receive arbitrary JavaScript values. Each parameter has a Web IDL type. A DOMString is a string of UTF-16 code units, while a USVString is a Unicode scalar-value string that cannot retain a lone surrogate.
const box = document.createElement("div");box.setAttribute("data-count", 5);console.log(typeof box.getAttribute("data-count"));Line 1 creates a detached element. Line 2 gives setAttribute the number 5. The binding converts that value to the DOMString "5". Line 3 therefore prints string, not number.
Replay a small model of the DOMString conversion used by setAttribute.
script
const text = String(value);const attribute = text;console.log(typeof attribute);const url = new URL("https://shop.example/?q=\uD800");console.log(url.search);Line 1 gives URL text containing one unmatched surrogate. The URL API converts that input to a USVString, replacing the invalid scalar position with U+FFFD. Line 2 prints ?q=%EF%BF%BD, the percent encoding of that replacement character.
A DOMString uses JavaScript's ordinary string conversion and can retain a lone surrogate code unit. A USVString first makes a DOMString, then replaces each lone surrogate with the replacement character. URL parsing needs scalar values, so it uses the stricter representation.
This is not a general instruction to sanitize every JavaScript string. It is one API-boundary rule. Keep the question narrow: which Web IDL type does this specific parameter name? The answer predicts the conversion before the URL algorithm begins.
const snacks = new Set(["tea", "cake"]);const blob = new Blob(snacks);console.log(blob.size);Line 1 creates a Set, which is iterable. Line 2 passes it to Blob; its sequence parameter consumes the iterable into a list of blob parts. Line 3 prints 7, because tea has three UTF-8 bytes and cake has four.
const letters = new Set(["a", "b"]);
const copied = [...letters];
console.log(copied.join(","));Line 1 makes a Set, whose iterator yields "a" and then "b". Line 2 spreads that iterable into a new Array. Line 3 prints a,b. A Web IDL sequence follows the same important idea: it accepts an iterable and builds a list of converted values rather than retaining the original Set object as the sequence.
| Type | Meaning | Small example |
|---|---|---|
| DOMString | A string of UTF-16 code units | setAttribute turns 5 into "5". |
| USVString | A string of Unicode scalar values | A lone surrogate becomes U+FFFD for URL text. |
| sequence<T> | A copied list built from an iterable | Blob accepts a Set of parts. |
Wrappers and browser objects
Many browsers implement DOM data using native browser objects. JavaScript receives a platform object that stands for that browser object. Developers often call this visible JavaScript object a wrapper.
const firstBody = document.body;const secondBody = document.body;console.log(firstBody === secondBody);Line 1 reads the body wrapper. Line 2 reads it again. Line 3 prints true. The same underlying document body is observed as the same JavaScript object, so equality and event listeners behave predictably.
This does not require you to know the browser's internal language. The Web IDL standard permits implementation choices. The observable promise is the JavaScript identity and interface behavior, not a particular C++ class layout.
The useful mental model is simple: JavaScript holds a platform object, and browser code owns the platform behavior behind it. Do not serialize a DOM node expecting to recreate its browser identity somewhere else.
const body = document.body;
body.lessonNote = "tea";
console.log(document.body.lessonNote);
console.log(body === document.body);
delete body.lessonNote;Line 1 reads the body once. Line 2 adds a temporary ordinary JavaScript property to that visible object. Line 4 reads body again through document.body and prints tea. Line 5 prints true; both names still refer to the same wrapper.
The final delete keeps this small example polite to the page. More importantly, it separates two facts: the property is ordinary JavaScript state on the wrapper, while the wrapper itself represents a stable browser platform object. Browser implementations may use C++ internally, but that is not a second object you manipulate directly from JavaScript.
Replay an instrumented wrapper-identity model. It explains observable JavaScript identity, not a browser's private native layout.
script
body.lessonNote = "tea";const again = document.body;console.log(again.lessonNote);console.log(body === again);Interface objects and prototypes
An interface object is the JavaScript value named after a Web IDL interface, such as HTMLElement. It is a function object, which is why typeof HTMLElement says function.
console.log(typeof HTMLElement);try { new HTMLElement();} catch (error) { console.log(error.name);}Line 1 prints function. Line 3 tries to construct an HTMLElement directly. HTML elements need browser creation rules, so line 5 prints TypeError after the catch. A function object can expose an interface without accepting new.
let current = document.body;for (let step = 0; step < 5; step += 1) { current = Object.getPrototypeOf(current); console.log(current.constructor.name);}Line 1 starts at the body object. Line 3 moves one prototype upward each time. Line 4 prints five constructor names: HTMLBodyElement, HTMLElement, Element, Node, and EventTarget.
Step through the interface prototypes above document.body.
script
let current = document.body; for (let step = 0; step < 5; step += 1) { current = Object.getPrototypeOf(current); names.push(current.constructor.name);} console.log(names.join(" -> "));That chain explains inherited methods. An element can call addEventListener because EventTarget behavior sits higher in the chain. It does not mean each body object stores a separate copy of every method.
The interface object and the interface prototype do different jobs. HTMLElement is the global interface object. Its prototype is the shared object in an element's prototype chain. Methods such as element operations live on those prototypes so the browser does not need to place a separate function on every element instance.
The constructor error is useful evidence rather than a dead end. Web IDL interface objects are function objects, but only interfaces with suitable constructor operations can make instances with new. The DOM instead makes elements through document creation methods, where the document can supply the right realm, owner document, and element-specific setup.
Legacy platform objects and live collections
Some older web objects act partly like ordinary JavaScript objects and partly like collections. Web IDL calls platform objects with indexed or named property support legacy platform objects. This behavior exists for compatibility and is not a pattern for new API design.
const list = document.createElement("ul");list.innerHTML = "<li>tea</li>";const live = list.getElementsByTagName("li");const snapshot = list.querySelectorAll("li");list.append(document.createElement("li"));console.log(live.length, snapshot.length);Line 3 gets a live HTMLCollection. Line 4 gets a static NodeList. Line 5 adds another list item. Line 6 prints 2 1: the live collection updated, while the querySelectorAll result kept its original match list.
const list = document.createElement("ul");
list.innerHTML = "<li>tea</li>";
const live = list.getElementsByTagName("li");
const snapshot = list.querySelectorAll("li");
list.append(document.createElement("li"));
console.log(live.length, snapshot.length);Added items: 1
Live HTMLCollection: 1
Static NodeList: 1
The live collection sees 1 list items. The static NodeList still has 1.
| Object | After DOM changes | Example |
|---|---|---|
| Live HTMLCollection | Updates when matching DOM changes | getElementsByTagName |
| Static NodeList | Keeps the matches from the query | querySelectorAll |
| Legacy platform object | Can expose indexed or named properties | Collections can act array-like without being arrays. |
Live does not mean reactive in a framework sense. Reading the collection later can produce different contents because the DOM changed. Convert it to an Array when you need a stable copy for a later calculation.
Indexed collection access is legacy platform-object behavior: a collection can answer collection[0] even though it is not an Array. Named access is another old compatibility shape. Treat those conveniences as browser API behavior, not as a reason to use arbitrary property indexing in new application APIs.
In the playground, the only changed input is the number of list items. Reset restores one item. The live result follows that input; the static result deliberately stays at the query-time snapshot. That contrast is the safe debugging question to ask when a list appears to move.
DOMException
DOMException is a platform error object with a name and message. It is separate from JavaScript's ordinary Error subclasses, though it has error-like behavior and can be checked with instanceof DOMException.
const stopped = new DOMException("Stopped", "AbortError");console.log(stopped.name, stopped instanceof DOMException);Line 1 creates an exception with the name AbortError. Line 2 prints AbortError true. Code should normally use the name to identify the web-platform condition, rather than relying on legacy numeric error codes.
try { document.createElement("1abc");} catch (error) { console.log(error.name);}Line 2 asks the DOM to create a name that starts with a digit. The DOM rejects that name. Line 4 prints InvalidCharacterError, a DOMException name defined for invalid characters.
A DOMException tells you an API contract was not met. It is not proof that every thrown browser error is a DOMException, and a normal JavaScript TypeError can still be the right error for a different kind of failure.
const problem = new DOMException("Bad name", "InvalidCharacterError");
console.log(problem.name);
console.log(problem.code);
console.log(problem instanceof DOMException);Line 1 constructs a platform exception with a message and name. Line 2 prints InvalidCharacterError. Line 3 prints the legacy numeric code 5. Line 4 prints true. In application code, prefer the descriptive name; codes exist for compatibility with older APIs.
The DOM's invalid-name example shows a browser-created exception, while this snippet constructs one directly. In both cases, the important branch is a stable name such as InvalidCharacterError, not a browser-specific message string.
The document.all exception
document.all is a compatibility relic. It returns an HTMLAllCollection, so property reads such as document.all.length work. Yet the HTML specification gives it special behavior for old feature-detection code.
console.log(typeof document.all);console.log(Boolean(document.all));console.log(typeof document.all.length);Line 1 prints undefined. Line 2 prints false. Line 3 prints number, because the collection still has a readable length. It is not literally the JavaScript value undefined.
The special [[IsHTMLDDA]] internal slot makes this possible. It affects typeof, Boolean conversion, and loose equality behavior. New code must not use it for feature detection; it exists only so old pages keep working.
In particular, document.all == null is true while strict equality with undefined is false. This is a narrowly specified compatibility exception, not a conversion rule you can recreate with a normal object, Proxy, or custom valueOf method.
Treat the double brackets as specification notation for an internal slot. JavaScript source cannot add [[IsHTMLDDA]] to an object. The HTML Standard gives it only to HTMLAllCollection so old pages that detect ancient browsers still behave as they historically did.
Practical debugging habits
Bindings become useful when an API surprises you. First inspect the API signature and the type of each argument. A number stored as a DOM attribute, a malformed URL string, and a live collection are all ordinary consequences of the binding contract.
When object identity matters, test it directly with strict equality. When inherited behavior matters, inspect prototypes with Object.getPrototypeOf. When a collection changes unexpectedly, check whether you have HTMLCollection or NodeList.
5becomes"5"for an attribute- A lone surrogate becomes U+FFFD in a URL
document.body === document.bodyHTMLElement.prototypeholds shared methods- A collection changes length after DOM changes
- Falsy object with
typeofequal to undefined
Put each card in the category that best explains it.
The published lessons on the DOM tree, node properties, attributes and properties, and the prototype chain give the everyday API practice behind these internals.
Common misconceptions
- “The DOM is pure JavaScript.” DOM objects are platform objects exposed through a JavaScript binding.
- “Every interface function accepts new.” An interface object can be a function and still throw when constructed.
- “All DOM collections are arrays.” They can be collection objects with different identity and liveness rules.
- “document.all is undefined.” It has selected undefined-like behavior, but remains an object-like collection.
- “DOMException is just Error.” It is a distinct web-platform interface with observable names.
These distinctions save debugging time because they point to the correct layer. A conversion problem belongs at the API boundary. A prototype problem belongs in interface inheritance. A moving query result belongs in collection liveness.
Practice exercises
What type does the final log print?
const box = document.createElement("div");
box.setAttribute("data-count", 5);
console.log(typeof box.getAttribute("data-count"));The answer is string. The DOMString binding converts the number before storing it.
A URL receives a lone surrogate. What value replaces it during USVString conversion?
The replacement is U+FFFD, the replacement character.
Your shopping list adds items after the query. Which API should you expect to update?
const live = list.getElementsByTagName("li");getElementsByTagName returns the live collection in this comparison.
What is the name of the created exception?
const stopped = new DOMException("Stopped", "AbortError");
console.log(stopped.name, stopped instanceof DOMException);The name is AbortError.
Why can this old collection be falsy while still having a length?
document.all is falsy because of its legacy [[IsHTMLDDA]] behavior, not because it is ordinary undefined.
A form value changes shape after a browser API call. What should you inspect before adding a workaround?
Inspect Web IDL and the API specification, then reproduce the small input conversion in the console.
Check your understanding
Use the binding contract to predict values instead of treating browser behavior as magic.
Question 1 of 8What is Web IDL for?
Choose an answer to see the explanation.
Question 2 of 8What does this print?
Read the code, then predictconsole.log(new URL("https://shop.example/?q=\uD800").search);Choose an answer to see the explanation.
Question 3 of 8Why does
new HTMLElement()throw?Choose an answer to see the explanation.
Question 4 of 8What does a live HTMLCollection do after a matching element is added?
Choose an answer to see the explanation.
Question 5 of 8What does the DOMException snippet print?
Read the code, then predictconst error = new DOMException("Stopped", "AbortError"); console.log(error.name, error instanceof DOMException);Choose an answer to see the explanation.
Question 6 of 8Why is
document.allunusual?Choose an answer to see the explanation.
Question 7 of 8What does a Web IDL sequence require from a JavaScript argument?
Choose an answer to see the explanation.
Question 8 of 8Which value should application code usually branch on for a DOMException?
Choose an answer to see the explanation.
Key takeaways
- Web IDL describes how browser APIs become JavaScript objects and methods.
- DOMString, USVString, and sequences each convert JavaScript input differently.
- DOM nodes are stable platform objects with interface prototypes.
- Live collections can change after DOM changes, unlike static query results.
- DOMException names describe web-platform failure conditions.
- document.all is a deliberate legacy compatibility exception.
Remember the one-liner.
A browser binding is the rulebook that lets JavaScript talk to platform objects.
Coming next: Realms, globals & WindowProxy.