cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

null inside the engine

Trace JavaScript null through V8 oddballs, typeof history, undetectable maps, document.all, null prototypes, and prototype lookups.

By the end, you can
  • 01
    Explain 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.
  • 02
    Read the checks engines compileConnect === null, == null, typeof x === "object", and document.all to bytecode mnemonics and map-bit tests.
  • 03
    Use 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.

Definition

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.

Real-life analogyOne shared empty label

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 oddball fieldsText
// 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 initializes the null oddballText
// 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);
Node-only %DebugPrint(null)JavaScript
%DebugPrint(null);
Stable parts of the debug outputText
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.

Coercion that follows the cached valuesPop out in the code editor (opens in a new tab)JavaScript
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.

How engines represent null
EngineRepresentationWhat this lesson uses it for
V8 nullRead-only Oddball object with kind kNull = 3.to_number is Smi 0, to_string is "null", and type_of is cached as "object".
V8 undefinedSeparate read-only Oddball with its own map.Its type_of cache is "undefined", and its map is also undetectable.
JavaScriptCoreImmediate ValueNull value, not a heap oddball.JSCJSValue.h defines ValueNull = OtherTag and stores it directly for JSNullTag.
SpiderMonkeyA tagged JS::Value null type.Value.h gives JSVAL_TYPE_NULL = 0x04 and JSVAL_TAG_NULL.
JSC and SpiderMonkey contrastText
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.

Teaching emulator for the old tag bitsPop out in the code editor (opens in a new tab)JavaScript
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 the 1995-style typeof null tag check
Step 0 of 4Ready
Your turn: follow the blue line

Step through a teaching model of the original tagged-word typeof decision. It models the historical source; it is not a modern engine layout.

Running in
  1. script
Next: line 21
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

Functions used for the bytecode probeJavaScript
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));
Stable bytecode mnemonicsText
strictNull: Ldar a0; TestNull; ReturnlooseNull: Ldar a0; TestUndetectable; ReturnobjectType: Ldar a0; TestTypeOf #7; Return

x === 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.

Node-only %GetUndetectable() proofJavaScript
const 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.

Browser-only real 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));
Spec source

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.

A null-prototype dictionaryPop out in the code editor (opens in a new tab)JavaScript
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);
Node-only dictionary-mode proofJavaScript
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.

Practical trade-off

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.

A real chain with a missing propertyPop out in the code editor (opens in a new tab)JavaScript
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);
Step through a property lookup that reaches null
Step 0 of 10Ready
Your turn: follow the blue line

Replay a real prototype lookup: one property is found on base, and one missing property walks all the way to null.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

Playground: which null check decides?
The selected checkPop out in the code editor (opens in a new tab)JavaScript
const value = null;console.log(typeof value);console.log(value == null);console.log(value === null);console.log(Boolean(value));
Observed resultnull
typeofobject
== nulltrue
=== nulltrue
Booleanfalse
ToNumber0
Which check?

TestNull answers `=== null`; TestUndetectable answers `== null`.

Try it yourself

null has typeof object. Loose null is true, strict null is true, and Boolean(...) is false.

Real JavaScript values are computed with the browser's operators. The document.all row is a model backed by Annex B and a Node %GetUndetectable probe.
Sort the null-internals facts
  • V8 initializes null's type_of field with "object".
  • null + 1 becomes 1 because null's ToNumber cache is Smi 0.
  • x == null compiles to TestUndetectable.
  • document.all === null is false even though document.all == null is true.
  • Object.create(null) has no inherited toString.
  • V8 reports %HasFastProperties(Object.create(null)) as false.
  • Object.getPrototypeOf(Object.prototype) === null.
  • A missing property read returns undefined after the chain reaches null.
Try it yourself
0 of 8 correct

Place each card by the engine idea it belongs to.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
Practical nullish check that keeps valid falsy valuesPop out in the code editor (opens in a new tab)JavaScript
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 === null when only deliberate null should match.
  • Use value == null when both null and undefined mean missing.
  • Do not use typeof value === "object" without a null guard.
  • Use Object.create(null) sparingly for dictionaries; consider Map for richer data structures.

Common misconceptions

  • “typeof null proves null is an object.” No. It is a preserved historical result. Property access on null still throws.
  • “== null is the same as a falsy check.” No. It catches nullish and undetectable values, not 0, "", or false.
  • “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.
Values that are easy to mix up
Valuetypeof== null=== nullToNumberToBooleanEngine note
null"object"truetrue0falseV8 null oddball map is undetectable.
undefined"undefined"truefalseNaNfalseV8 undefined oddball map is undetectable.
document.all"undefined"truefalseNaNfalseAnnex B [[IsHTMLDDA]]; V8 API objects use the undetectable bit.
Object.create(null)"object"falsefalseTypeErrortrueNull prototype, dictionary map, not undetectable.
Misconception map
IdeaAccurate meaningDo not confuse it with
typeof nullA required historical result that returns "object".A sign that null has object methods or properties.
value == nullAn intentional check for nullish or undetectable values.A general falsy check; it does not match 0, "", or false.
Null prototypeAn object whose [[Prototype]] is null.The null value itself, or a value that is loosely equal to null.
Dictionary modeV8's slow-properties storage for null-prototype dictionaries.A JavaScript Map; use Map when you need non-string keys or frequent churn.
Real-life analogyThe end of a queue

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 EXERCISES
Exercise 1 · Warm-upPredict null coercion

Predict the three outputs in order.

Starter codePop out in the code editor (opens in a new tab)JavaScript
console.log(null + 1);
console.log(null >= 0);
console.log(null == 0);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upLoose versus strict missing pair

    What do the loose and strict comparisons print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    console.log(null == undefined);
    console.log(null === undefined);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeRead a null-prototype dictionary

      Predict both outputs.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const dict = Object.create(null);
      console.log("toString" in dict);
      console.log(Object.getPrototypeOf(dict) === null);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeUse the old tag model

        What does the model print for the all-zero null word?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const JSVAL_OBJECT = 0;
        const nullWord = 0x00000000;
        console.log((nullWord & 0x7) === JSVAL_OBJECT ? "object" : "other");

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · PracticeChoose the document.all-aware check

          Which check catches null, undefined, and the legacy document.all behavior?

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeExplain a missing lookup

            What value comes back for child.missing?

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            const base = { answer: 42 };
            const child = Object.create(base);
            console.log(child.answer);
            console.log(child.missing);

            Answer, then press Check. Spacing and letter case don’t matter.

              Quiz: check your understanding

              8 QUESTIONS
              null internals quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What is V8's null value in this lesson?

                Choose an answer to see the explanation.

              2. Question 2 of 8What do these null coercions print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                console.log(null + 1);
                console.log(null >= 0);
                console.log(null == 0);

                Choose an answer to see the explanation.

              3. Question 3 of 8Why does typeof null return "object" today?

                Choose an answer to see the explanation.

              4. Question 4 of 8Which bytecode did Node 22/V8 12.4 print for x == null?

                Choose an answer to see the explanation.

              5. Question 5 of 8What does this null-prototype code print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const dict = Object.create(null);
                console.log("toString" in dict);
                console.log(Object.getPrototypeOf(dict) === null);

                Choose an answer to see the explanation.

              6. Question 6 of 8Which observable behavior belongs to document.all?

                Choose an answer to see the explanation.

              7. Question 7 of 8What does this lookup snippet print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const base = { answer: 42 };
                const child = Object.create(base);
                console.log(child.answer);
                console.log(child.missing);

                Choose an answer to see the explanation.

              8. 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 null result comes from the original all-zero null word sharing the object tag.
              • === null is TestNull; == null is TestUndetectable.
              • Browser document.all is 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.

              CompleteFrontend Clear concepts. Working examples.