null inside the engine
Trace JavaScript null through V8 oddballs, typeof history, undetectable maps, document.all, null prototypes, and prototype lookups.
- 01Explain the real null valueDescribe V8's read-only null oddball, its cached coercion fields, its undetectable map bit, and how other engines encode null differently.
- 02Read the checks engines compileConnect
=== null,== null,typeof x === "object", and document.all to bytecode mnemonics and map-bit tests. - 03Use null-shaped objects safelyKnow when null-prototype dictionaries help, why V8 gives them dictionary maps, and how prototype lookups stop at null.
null is one oddball
null is JavaScript's deliberate empty value. Inside V8 12.4 it is not allocated every time you write it. The engine keeps one read-only oddball object for null, and every JavaScript null value is a tagged pointer to that object.
The null oddball is V8's single internal heap object for JavaScript null. Its cached fields say how null converts to number, string, and typeof; its map is marked undetectable so loose null checks can be fast.
If you want the beginner meaning first, read null for beginners. This expert lesson builds on the tagged-value background from Tagged values, Smis & heap numbers and follows undefined inside the engine in the internals track.
Several boxes can use the same empty label. The label is not remade for each box; each box points to the shared label.
- In real life: One empty label is kept
- In JavaScript: One read-only null value ends prototype chains
- In real life: Many boxes use the same label
- In JavaScript: Every occurrence of null points to the same oddball
- In real life: The label has fixed text
- In JavaScript: The oddball caches ToNumber, ToString, and typeof results
Where the analogy stops: A label is visible and physical. The null oddball is an implementation detail; JavaScript code only observes the language value.
The null oddball: fields and cached values
V8's oddball header says oddballs describe null, undefined, true, and false. The same file names cached to_number, to_string, type_of, and kind fields. In V8 12.4, null's kind is kNull = 3.
// V8 12.4.254.21, src/objects/oddball.h// The Oddball describes objects null, undefined, true, and false.// [to_number_raw]: Cached raw to_number computed at startup.// [to_string]: Cached to_string computed at startup.// [to_number]: Cached to_number computed at startup.// [typeof]: Cached type_of computed at startup.static constexpr uint8_t kNull = 3;static constexpr uint8_t kUndefined = 4;The setup code in setup-heap-internal.cc creates a separate null_map, marks it undetectable, then initializes null with to_number as Smi zero, to_string as "null", type_of as "object", and kind kNull.
// V8 12.4.254.21, src/heap/setup-heap-internal.ccALLOCATE_AND_SET_ROOT(Map, null_map, Map::kSize);InitializePartialMap(null_map, meta_map, ODDBALL_TYPE, sizeof(Null));roots.null_map()->set_is_undetectable(true);Oddball::Initialize(isolate(), factory->null_value(), "null", handle(Smi::zero(), isolate()), "object", Oddball::kNull);%DebugPrint(null)JavaScript%DebugPrint(null);DebugPrint: ... [Oddball] in ReadOnlySpace: #null... [Map] in ReadOnlySpace - type: ODDBALL_TYPE - stable_map - undetectable - non-extensible - prototype: ... <null>The child-process test asserts the stable words: [Oddball], ReadOnlySpace, #null, ODDBALL_TYPE, and undetectable. It does not assert addresses or instance size because Node builds can differ; this Node build also has pointer compression and the V8 sandbox disabled, while Chrome usually enables them and may run a newer V8.
console.log(Number(null));console.log(String(null));console.log(null + 1);console.log(null >= 0);console.log(null == 0);Line 1 reads null's ToNumber behavior: 0. Line 2 reads ToString: "null". Lines 3 and 4 explain why null + 1 is 1 and null >= 0 is true. Line 5 reminds you that loose equality has its own special nullish rule, so null == 0 is false.
| Engine | Representation | What this lesson uses it for |
|---|---|---|
| V8 null | Read-only Oddball object with kind kNull = 3. | to_number is Smi 0, to_string is "null", and type_of is cached as "object". |
| V8 undefined | Separate read-only Oddball with its own map. | Its type_of cache is "undefined", and its map is also undetectable. |
| JavaScriptCore | Immediate ValueNull value, not a heap oddball. | JSCJSValue.h defines ValueNull = OtherTag and stores it directly for JSNullTag. |
| SpiderMonkey | A tagged JS::Value null type. | Value.h gives JSVAL_TYPE_NULL = 0x04 and JSVAL_TAG_NULL. |
JavaScriptCore JSCJSValue.h: ValueNull = OtherTag; JSValue(JSNullTag) stores ValueNull. SpiderMonkey js/public/Value.h: JSVAL_TYPE_NULL = 0x04; JSVAL_TAG_NULL stores a null payload. V8 12.4 oddball.h: Null is an Oddball subclass; kind kNull = 3.JavaScriptCore's JSCJSValue.h uses an immediate ValueNull. SpiderMonkey's Value.h names JSVAL_TYPE_NULL = 0x04. Same JavaScript semantics, different engine storage.
typeof null: a 1995 type tag
The modern spec still says typeof null returns "object". The historical reason is older than V8. Axel Rauschmayer's archived 2ality article quotes the original SpiderMonkey-style tags: object tag 000, int tag 1, double tag 010, string tag 100, and boolean tag 110. The old Netscape source mirror shows JSVAL_OBJECT 0x0 and JSVAL_NULL OBJECT_TO_JSVAL(0) in jsapi.h, while jsapi.c checks JSVAL_IS_OBJECT before number, string, or boolean.
const JSVAL_OBJECT = 0x0;const JSVAL_INT = 0x1;const JSVAL_STRING = 0x4;const JSVAL_BOOLEAN = 0x6;const JSVAL_VOID = 0xfffffff0; function tagBits(word) { return (word & 0x7).toString(2).padStart(3, "0");} function typeof1995(word) { const tag = word & 0x7; if (word === JSVAL_VOID) return "undefined"; if (tag === JSVAL_OBJECT) return "object"; if (word & JSVAL_INT) return "number"; if (tag === JSVAL_STRING) return "string"; if (tag === JSVAL_BOOLEAN) return "boolean"; return "number";} const nullWord = 0x00000000;console.log("null", tagBits(nullWord), typeof1995(nullWord));console.log("object", tagBits(0x00001000), typeof1995(0x00001000));console.log("string", tagBits(0x00003004), typeof1995(0x00003004));Step through a teaching model of the original tagged-word typeof decision. It models the historical source; it is not a modern engine layout.
script
const JSVAL_OBJECT = 0x0;const JSVAL_INT = 0x1;const JSVAL_STRING = 0x4;const JSVAL_BOOLEAN = 0x6;const JSVAL_VOID = 0xfffffff0; function tagBits(word) { return (word & 0x7).toString(2).padStart(3, "0");} function typeof1995(word) { const tag = word & 0x7; if (word === JSVAL_VOID) return "undefined"; if (tag === JSVAL_OBJECT) return "object"; if (word & JSVAL_INT) return "number"; if (tag === JSVAL_STRING) return "string"; if (tag === JSVAL_BOOLEAN) return "boolean"; return "number";} console.log("null", tagBits(nullWord), typeof1995(nullWord));console.log("object", tagBits(0x00001000), typeof1995(0x00001000));console.log("string", tagBits(0x00003004), typeof1995(0x00003004));The emulator is deliberately labelled as a model. It maps values to a 32-bit word, reads the old low tag bits, and shows why an all-zero null word goes down the object path. Brendan Eich described an opt-in Harmony change on es-discuss; the old wiki proposal said, “With opt-in to Harmony, typeof null === "null".” That proposal is archived at harmony:typeof_null and did not become the language because web compatibility won.
Bytecode for null checks
V8's Ignition bytecode makes three common checks visibly different. The test runs Node with --print-bytecode and filters for each function name. It asserts only stable mnemonics, not offsets.
function strictNull(x) { return x === null;} function looseNull(x) { return x == null;} function objectType(x) { return typeof x === "object";} console.log(strictNull(null));console.log(looseNull(undefined));console.log(objectType(null));strictNull: Ldar a0; TestNull; ReturnlooseNull: Ldar a0; TestUndetectable; ReturnobjectType: Ldar a0; TestTypeOf #7; Returnx === null compiles to TestNull, a strict pointer comparison with the null constant.
x == null compiles to TestUndetectable, which V8's interpreter generator comments as “null, undefined or document.all”.
typeof x === "object" compiles to TestTypeOf #7; in V8's object branch, null returns true before receiver-map checks.
Undetectable maps and document.all
The map bit is the bridge between == null and document.all. V8's Map comment says an undetectable object has typeof undefined and ToBoolean false, useful for document.all. The public V8 API has ObjectTemplate::MarkAsUndetectable in api.cc.
%GetUndetectable() proofJavaScriptconst value = %GetUndetectable();console.log(typeof value);console.log(value == null);console.log(value == undefined);console.log(value === null);console.log(Boolean(value));%DebugPrint(value);The test proves that V8's model value has typeof "undefined", is loosely equal to null and undefined, is not strictly equal to null, and is falsy. Its debug print includes undetectable and callable on the API-object map.
document.allJavaScript// Browser-only Annex B behavior. Do not paste this into Node.console.log(typeof document.all);console.log(document.all == null);console.log(document.all == undefined);console.log(document.all === null);console.log(Boolean(document.all));ECMAScript Annex B defines the legacy [[IsHTMLDDA]] internal slot. That is why browser document.all is falsy, reports typeof "undefined", and matches loose null without being strictly equal to it.
Null prototypes and dictionary-mode objects
Object.create(null) and object literals with { __proto__: null } make real objects whose prototype is null. They are useful as safe string-key dictionaries because inherited names such as toString are not in the chain. They are not the same value as null.
const dict = Object.create(null);dict.answer = 42;console.log(Object.getPrototypeOf(dict) === null);console.log("toString" in dict);console.log(dict.answer); const literal = { __proto__: null, answer: 42 };console.log(Object.getPrototypeOf(literal) === null);const created = Object.create(null);const literal = { __proto__: null };const ordinary = {};console.log(%HasFastProperties(created));console.log(%HasFastProperties(literal));console.log(%HasFastProperties(ordinary));%DebugPrint(created);The test proves %HasFastProperties(Object.create(null)) and %HasFastProperties({ __proto__: null }) are false, while {} is true. V8's bootstrapper comments “Set up slow map for Object.create(null) instances without in-object properties” and stores it as slow_object_with_null_prototype_map.
Null-prototype objects are good for avoiding inherited-name surprises and some prototype-pollution bugs. For frequent additions, deletions, non-string keys, or ordered iteration, a JavaScript Map is often clearer. See Prototype pollution for the security angle.
Lookups that end at null
Property lookup starts at the receiver, checks its map and own storage, then follows the prototype pointer. Eventually the chain reaches null. The spec's OrdinaryGet returns undefined when the next prototype is null. V8 maps also carry a prototype_validity_cell, and inline caches can guard cached prototype-chain answers with that cell.
const base = { shared: "base value" };const middle = Object.create(base);const child = Object.create(middle); console.log(child.shared);console.log(child.missing);console.log(Object.getPrototypeOf(Object.prototype) === null);Replay a real prototype lookup: one property is found on base, and one missing property walks all the way to null.
script
const middle = Object.create(base);const child = Object.create(middle); console.log(child.shared);console.log(child.missing);console.log(Object.getPrototypeOf(Object.prototype) === null);Link this to Prototype internals for maps and prototype pointers, and to Inline caches for why the engine wants a reusable “not found until this prototype chain changes” answer.
Playground and practical use
Pick a value and compare exactly what the engine-level checks are trying to answer: typeof, loose null, strict null, Boolean conversion, numeric conversion, and the bytecode or map-bit check behind it.
const value = null;console.log(typeof value);console.log(value == null);console.log(value === null);console.log(Boolean(value));objecttruetruefalse0TestNull answers `=== null`; TestUndetectable answers `== null`.
null has typeof object. Loose null is true, strict null is true, and Boolean(...) is false.
- V8 initializes null's
type_offield with"object". null + 1becomes1because null's ToNumber cache is Smi0.x == nullcompiles toTestUndetectable.document.all === nullis false even thoughdocument.all == nullis true.Object.create(null)has no inheritedtoString.- V8 reports
%HasFastProperties(Object.create(null))as false. Object.getPrototypeOf(Object.prototype) === null.- A missing property read returns
undefinedafter the chain reaches null.
Place each card by the engine idea it belongs to.
function describeResult(value) { if (value == null) { return "missing"; } return "present: " + value;} console.log(describeResult(null));console.log(describeResult(undefined));console.log(describeResult(0));console.log(describeResult(""));- Use
value === nullwhen only deliberate null should match. - Use
value == nullwhen bothnullandundefinedmean missing. - Do not use
typeof value === "object"without a null guard. - Use
Object.create(null)sparingly for dictionaries; considerMapfor richer data structures.
Common misconceptions
- “
typeof nullproves null is an object.” No. It is a preserved historical result. Property access on null still throws. - “
== nullis the same as a falsy check.” No. It catches nullish and undetectable values, not0,"", orfalse. - “document.all is just undefined.” No. It is a legacy host object with Annex B behavior; strict equality still sees it is not null.
- “A null-prototype object is null.” No. It is a real object whose prototype pointer is null.
- “Dictionary mode means slow is always bad.” No. It is a trade-off V8 chooses for dictionary-shaped objects.
| Value | typeof | == null | === null | ToNumber | ToBoolean | Engine note |
|---|---|---|---|---|---|---|
null | "object" | true | true | 0 | false | V8 null oddball map is undetectable. |
undefined | "undefined" | true | false | NaN | false | V8 undefined oddball map is undetectable. |
document.all | "undefined" | true | false | NaN | false | Annex B [[IsHTMLDDA]]; V8 API objects use the undetectable bit. |
Object.create(null) | "object" | false | false | TypeError | true | Null prototype, dictionary map, not undetectable. |
| Idea | Accurate meaning | Do not confuse it with |
|---|---|---|
typeof null | A required historical result that returns "object". | A sign that null has object methods or properties. |
value == null | An intentional check for nullish or undetectable values. | A general falsy check; it does not match 0, "", or false. |
| Null prototype | An object whose [[Prototype]] is null. | The null value itself, or a value that is loosely equal to null. |
| Dictionary mode | V8's slow-properties storage for null-prototype dictionaries. | A JavaScript Map; use Map when you need non-string keys or frequent churn. |
Follow the queue from one person to the next. At the end, there is no next person, so the search stops. JavaScript property lookup stops at null in the same way.
- In real life: Each person points to the next person
- In JavaScript: Each object points to a prototype
- In real life: The last person has no next person
- In JavaScript: The final prototype pointer is null
- In real life: A separate queue starts with no earlier person
- In JavaScript: A null-prototype dictionary avoids inherited names
Where the analogy stops: People move through a queue. Engines use maps, descriptors, prototype cells, and inline caches.
Practice exercises
6 EXERCISESPredict the three outputs in order.
console.log(null + 1);
console.log(null >= 0);
console.log(null == 0);The lines print 1, true, and false in that order.
What do the loose and strict comparisons print?
console.log(null == undefined);
console.log(null === undefined);The loose comparison is true; the strict comparison is false.
Predict both outputs.
const dict = Object.create(null);
console.log("toString" in dict);
console.log(Object.getPrototypeOf(dict) === null);The dictionary does not inherit toString, and its prototype is exactly null: false, then true.
What does the model print for the all-zero null word?
const JSVAL_OBJECT = 0;
const nullWord = 0x00000000;
console.log((nullWord & 0x7) === JSVAL_OBJECT ? "object" : "other");The all-zero null word has the old object tag, so the model prints object.
Which check catches null, undefined, and the legacy document.all behavior?
Use value == null when you intentionally want null, undefined, and legacy undetectable document.all behavior.
What value comes back for child.missing?
const base = { answer: 42 };
const child = Object.create(base);
console.log(child.answer);
console.log(child.missing);The missing property read produces undefined after the prototype walk reaches null.
Quiz: check your understanding
8 QUESTIONSQuestion 1 of 8What is V8's
nullvalue in this lesson?Choose an answer to see the explanation.
Question 2 of 8What do these null coercions print?
Read the code, then predictconsole.log(null + 1); console.log(null >= 0); console.log(null == 0);Choose an answer to see the explanation.
Question 3 of 8Why does
typeof nullreturn"object"today?Choose an answer to see the explanation.
Question 4 of 8Which bytecode did Node 22/V8 12.4 print for
x == null?Choose an answer to see the explanation.
Question 5 of 8What does this null-prototype code print?
Read the code, then predictconst dict = Object.create(null); console.log("toString" in dict); console.log(Object.getPrototypeOf(dict) === null);Choose an answer to see the explanation.
Question 6 of 8Which observable behavior belongs to
document.all?Choose an answer to see the explanation.
Question 7 of 8What does this lookup snippet print?
Read the code, then predictconst base = { answer: 42 }; const child = Object.create(base); console.log(child.answer); console.log(child.missing);Choose an answer to see the explanation.
Question 8 of 8When is
Object.create(null)a good tool?Choose an answer to see the explanation.
Key takeaways
- V8 stores one read-only null oddball with cached ToNumber zero, ToString
"null", and typeof"object". - The famous
typeof nullresult comes from the original all-zero null word sharing the object tag. === nullisTestNull;== nullisTestUndetectable.- Browser
document.allis an Annex B legacy object that is falsy and loosely nullish without being null. - Null-prototype objects are useful dictionaries, and property lookups stop when the prototype pointer is null.
Remember the one-liner.null is one deliberate empty value; engines give it special storage and checks, but prototype chains also use null as their stop sign.
Up next: Strings inside the engine, where values become one-byte strings, two-byte strings, ropes, slices, and table entries.