undefined inside the engine
Trace how V8 stores undefined as a read-only oddball, emits LdaUndefined bytecode, separates holes, and handles missing values.
- 01Explain the one undefined valueSay why every
undefinedin V8 is a pointer to the same read-only oddball, made once when V8 starts. - 02Read the bytecodeRecognize
LdaUndefined,LdaTheHole, the TDZ check, how missing arguments get filled, and a local variable namedundefined. - 03Tell gaps from missing valuesTell apart array gaps, missing properties, missing arguments, names that were never declared, and the global
undefinedproperty.
One undefined, many ways to get it
You know undefined from everyday JavaScript. This lesson shows how a JavaScript engine stores it. An engine is the program that runs your code. We use V8, the engine inside Chrome and Node.js. Every fact here was checked in Node 22, which uses V8 version 12.4.
Inside V8, there is exactly one undefined. V8 makes it once, when it starts. V8 calls a few special built-in values oddballs, and undefined is one of them. A variable that holds undefined only stores a pointer to it. A pointer is a memory address: it says where a value is. So getting undefined never makes a new value.
In V8, undefined is one oddball object that lives in read-only space: a part of memory that nothing is allowed to change. The object keeps a few ready-made answers:
- As a number, it is
NaN. - As a string, it is
"undefined". typeofgives"undefined".
V8's small instructions usually fetch it with one instruction, LdaUndefined, or find it already waiting in a slot. You'll meet both later in this lesson.
const fromIdentifier = undefined;const fromVoid = void 0;function noReturn() {} console.log(fromIdentifier === fromVoid);console.log(noReturn());console.log(Object.is(fromIdentifier, undefined));Line 1 stores undefined by name. Line 2 uses the void operator: void runs the expression after it and always gives undefined. Line 3 makes a function with no return.
- Line 5 prints
true: the first two ways give the same value. - Line 6 prints
undefined: a function that reaches its end withoutreturngivesundefined. - Line 7 prints
true.Object.ischecks whether two values are exactly the same value.
undefined, which V8 made once when it started.If you want the everyday rules for undefined first, read undefined. This lesson also uses the idea of a tagged pointer: a stored pointer with a small mark that says "this is a pointer, not a small number". The Tagged values lesson explains it.
A team shares one blank note on a board. When someone has nothing to write, they point to that note. They don't make a new blank note each time.
- In real life: There is one blank note on the board
- In JavaScript: There is one
undefinedin V8 - In real life: Anyone can point to that note
- In JavaScript: Any variable, property, or slot can hold a pointer to it
- In real life: Pointing to it makes no new note
- In JavaScript: Getting
undefinednever makes a new value
One more detail about the test machine. Node 22 here runs V8 12.4.254.21 without pointer compression, a trick that stores pointers in fewer bytes. Chrome uses newer V8 versions, usually with pointer compression and the sandbox, a fence that limits which memory V8 can touch. So memory addresses and object sizes differ. What stays the same: the names of V8's types, the names of its instructions, and the rules in its source code.
Where V8 keeps undefined
V8 is written in the C++ language, and its source code is public. So we can read how it builds undefined. The file oddball.h describes oddballs. Its comment says that four values are oddballs: null, undefined, true, and false.
Each oddball stores a few ready-made answers, so V8 never has to work them out again:
to_number_rawandto_number: the value as a number. Forundefined, that isNaN.to_string: the value as a string,"undefined".type_of: whattypeofgives, also"undefined".kind: a small number that says which oddball this is. Forundefined, it iskUndefined = 4.
V8 also gives every object a map: V8's record of a value's type and shape. undefined gets its own map, undefined_map. That map is marked undetectable. This mark means "treat this value like undefined in loose checks such as x == null". The map of null has the same mark.
%DebugPrint(undefined);%DebugPrint is a special V8 helper. It prints what V8 stores for a value. It only works when you start Node with node --allow-natives-syntax, so this block can't run on this page. The lesson's test runs it and checks the parts that are the same on every run:
[Oddball] in ReadOnlySpace: #undefined: it is the oddball, kept in read-only space.type: ODDBALL_TYPE: the map says its type is oddball.stable_map,undetectable, andnon-extensible: the map never changes, has the undetectable mark, and can't get new properties.prototype: <null>: it has no prototype to look things up in.
You'll see the same kind of map again in null inside the engine. That is why both null and undefined are "undetectable" inside V8.
// V8 12.4: 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 kUndefined = 4; // V8 12.4: src/heap/setup-heap-internal.ccInitializePartialMap(undefined_map, meta_map, ODDBALL_TYPE, sizeof(Undefined));roots.undefined_map()->set_is_undetectable(true);Oddball::Initialize(isolate(), factory->undefined_value(), "undefined", factory->nan_value(), "undefined", Oddball::kUndefined);These lines come from two V8 files. In plain words, the highlighted lines say:
- Line 2: the oddball type covers
null,undefined,true, andfalse. - Line 7: the kind number for
undefinedis 4. - Line 10: make a map called
undefined_map, with the typeODDBALL_TYPE. - Line 11: mark that map as undetectable.
- Lines 12 and 13: fill in the ready-made answers: the string
"undefined", the numberNaN, thetypeoftext"undefined", and the kind.
The second file, setup-heap-internal.cc, builds these maps and values when V8 starts. The debug output above shows that this Node build keeps undefined in read-only space. V8's read-only serializer saves that read-only memory into the startup snapshot: a saved copy of memory that V8 loads every time it starts.
Other engines store undefined differently. The next block shows two of them. JavaScriptCore is Safari's engine, and SpiderMonkey is Firefox's engine.
// JavaScriptCore runtime/JSCJSValue.h// False: 0x06// True: 0x07// Undefined: 0x0a// Null: 0x02static constexpr int32_t OtherTag = 0x2;static constexpr int32_t UndefinedTag = 0x8;static constexpr int32_t ValueUndefined = OtherTag | UndefinedTag; // SpiderMonkey js/public/Value.henum JSValueType : uint8_t { JSVAL_TYPE_UNDEFINED = 0x03, JSVAL_TYPE_NULL = 0x04,};JSVAL_TAG_UNDEFINED = JSVAL_TAG_MAX_DOUBLE | JSVAL_TYPE_UNDEFINED;Numbers that start with 0x are written in hexadecimal (base 16). For example, 0x0a is 10.
- In JavaScriptCore's JSCJSValue.h, the comment on line 4 lists
Undefined: 0x0a. Line 8 builds it asValueUndefined = OtherTag | UndefinedTag. This is not a pointer. The stored value itself is the number0x0a. - In SpiderMonkey's Value.h, line 12 gives
undefinedthe type numberJSVAL_TYPE_UNDEFINED = 0x03. Line 15 turns that into a mark calledJSVAL_TAG_UNDEFINED.
So the JavaScript value is the same in every engine. Only the way it is stored in memory is different. The table below has one row per value or engine. Read each row as: "this is how it is stored, and this is the proof."
| Which value or engine | How it is stored | What the source or test shows |
|---|---|---|
V8's undefined | One oddball object in read-only memory. Every undefined is a pointer to it. | Its type is ODDBALL_TYPE and its kind is kUndefined = 4. It keeps ready-made answers: NaN, "undefined", and the typeof text. |
V8's hole, the_hole_value | A separate Hole object in read-only memory. | V8 uses it to mark array gaps and the TDZ. Your code never gets it. |
| JavaScriptCore (Safari) | Not a pointer. The stored value is a fixed number. | ValueUndefined = OtherTag | UndefinedTag, which the comment lists as 0x0a. |
| SpiderMonkey (Firefox) | A stored value with a type mark. | JSVAL_TYPE_UNDEFINED = 0x03. On 64-bit machines it is combined into the mark JSVAL_TAG_UNDEFINED. |
Where bytecode gets undefined
Before V8 runs a function, it turns it into bytecode: a list of small, simple instructions. V8's interpreter, called Ignition, runs these instructions one at a time. Each instruction has a short name. For example, LdaUndefined means "load undefined".
The instructions in this lesson use two kinds of slots:
- The accumulator: one special slot that holds the value the interpreter is working on right now.
- Registers: numbered slots for local variables, written
r0,r1, and so on.
Names that start with Lda load a value into the accumulator. Return gives back whatever is in the accumulator. The lesson's test asks Node to print the bytecode of four small functions. All four print LdaUndefined, then Return:
- a bare
return; - a function that reaches its end without
return return void 0return undefined, whenundefinedmeans the usual global one
return; LdaUndefined, Returnfall off the end LdaUndefined, Returnreturn void 0 LdaUndefined, Returnreturn undefined LdaUndefined, Returnlet x; return x LdaUndefined, Star0, Returnx === undefined Ldar, TestUndefined, ReturnEach row shows a piece of code on the left and its instructions on the right. "Fall off the end" means the function reaches its last line without a return.
- Lines 1 to 4: load
undefinedinto the accumulator, then return it. - Line 5: load
undefined, thenStar0stores it in registerr0, which holdsx. Then return it. - Line 6:
Ldarloadsxinto the accumulator.TestUndefinedchecks whether it isundefinedand leavestrueorfalse. Then return that answer.
There is one exception. You are allowed to make a local variable called undefined. Then the name means your variable, not the real undefined. We say the local variable shadows the global one. So function f() { let undefined = 5; return undefined; } returns 5. Its bytecode uses LdaSmi (load a small whole number), Star0, and later Ldar r0. It never loads the real undefined.
A let or const variable can't be read before its line runs. Reading it too early throws a ReferenceError. The time before the line runs is called the temporal dead zone, or TDZ. V8 marks a variable in its TDZ with a different value: the hole. For const r = y; let y = 1;, Node prints these instructions:
LdaTheHole: load the hole.Star1: store it in registerr1, the slot fory.ThrowReferenceErrorIfHole: when the code readsy, throw aReferenceErrorif the slot still holds the hole.
The hole lets V8 tell "not set up yet" apart from the real undefined. You'll learn more about the hole below.
Registers and missing arguments
When a function is called, V8 gives that call a stack frame: a block of memory for that one call. The registers live in the stack frame. Before the first instruction runs, V8 runs a short piece of setup code called the interpreter entry trampoline. One of its jobs is to put undefined into every register.
V8's source has this setup code for each kind of processor. The block shows two: x64 (most desktop and laptop computers) and arm64 (phones and newer Macs). Both say the same thing.
// V8 12.4: src/builtins/x64/builtins-x64.cc// If ok, push undefined as the initial value for all register file entries.__ LoadRoot(kInterpreterAccumulatorRegister, RootIndex::kUndefinedValue);__ Push(kInterpreterAccumulatorRegister);// The accumulator is already loaded with undefined. // V8 12.4: src/builtins/arm64/builtins-arm64.cc// If ok, push undefined as the initial value for all register file entries.__ LoadRoot(kInterpreterAccumulatorRegister, RootIndex::kUndefinedValue);__ PushMultipleTimes(kInterpreterAccumulatorRegister, x11);// The accumulator is already loaded with undefined.In plain words, the highlighted lines say:
- Lines 2 and 8: the comment says "push
undefinedas the starting value for every register". - Lines 3 and 9:
LoadRootfetchesundefined. A root is a value the engine always keeps ready, such asundefined. - Lines 4 and 10: push that value into the register slots.
- Lines 5 and 11: the accumulator now holds
undefinedtoo.
So every register starts as undefined. A register never starts with leftover data from earlier code.
Now the missing arguments. A function's parameters are the names in its brackets, like name, role, city. What happens if you pass fewer values than that? The missing parameters become undefined. V8's arguments adaptor frame article explains how. Before V8 version 8.9, V8 built an extra stack frame to handle this. Since 8.9, that extra frame is gone. The call setup fills the missing parameters with undefined itself. In the x64 source, that call setup code is named InvokePrologue.
// V8 12.4: src/codegen/x64/macro-assembler-x64.cc// Underapplication. Move the arguments already in the stack, including the// receiver and the return address.// Fill remaining expected arguments with undefined values.LoadRoot(kScratchRegister, RootIndex::kUndefinedValue);movq(Operand(r8, expected_parameter_count, times_system_pointer_size, 0), kScratchRegister);In plain words, the highlighted lines say:
- Line 4: the comment says "fill the remaining expected arguments with
undefined". - Line 5: fetch
undefinedinto a spare register. - Lines 6 and 7: write it into the slot of the next missing parameter. V8 repeats this for each missing one.
makeBadge has three parameters. Before the function starts, V8 puts undefined into the two that got nothing.A call that passes fewer values than the function has parameters. The values are real. The frame drawing is a simple model, not real engine memory.
script
function makeBadge(name, role, city) { const stackSlots = [name, role, city]; return stackSlots.map(String).join(" | ");} Press Step to walk through the replay. It calls makeBadge("Ada") with one value. Then name is "Ada", and role and city are undefined. These values come from really running the function. The frame drawing beside the code is a simple model, not real engine memory.
The hole is not undefined
You met the hole in the TDZ example. The hole is a marker: a special value V8 uses only inside itself, to mean "nothing is here yet" or "nothing is here at all". Your code never gets the hole itself. When your code reads a slot that holds the hole, V8 follows a rule first. The rule either gives a normal result or throws an error.
In V8 12.4, the hole is not an oddball. It has its own type, Hole. V8 also makes several different holes, one for each job. Here are the ones in the source below:
the_hole_value: the main hole, used for gaps in arrays and for the TDZ.property_cell_hole_value,hash_table_hole_value,promise_hole_value,uninitialized_value,arguments_marker,optimized_out, andstale_register: more holes, each used by one part of V8.
// V8 12.4: src/objects/hole.hclass Hole : public HeapObject { // Holes store NaN at the HeapNumber value offset for optimized code. DECL_FIELD_OFFSET_TQ(RawNumericValue, HeapObject::kHeaderSize, "float64")}; // V8 12.4: src/heap/setup-heap-internal.ccHole::Initialize(isolate(), factory->the_hole_value(), factory->hole_nan_value());set_property_cell_hole_value(*factory->NewHole());set_hash_table_hole_value(*factory->NewHole());set_promise_hole_value(*factory->NewHole());set_uninitialized_value(*factory->NewHole());set_arguments_marker(*factory->NewHole());set_optimized_out(*factory->NewHole());set_stale_register(*factory->NewHole());In plain words, the highlighted lines say:
- Line 2:
Holeis its own kind of object. It is not anOddball. - Line 8: set up the main hole,
the_hole_value. - Lines 9 to 15: make one more separate hole for each of the other jobs.
let x; xvoid 0- the second parameter, when the call passed only one value
({}).missing, after nothing in the prototype chain has it- the middle slot in
[1, , 3] - an array item after
delete array[1] - a
letvariable before its line runs - reading
doesNotExist [1, , 3][1]afterArray.prototype[1] = "surprise"
Sort each case. Does your code get the real undefined? Does V8 keep a hole there? Or is there no undefined value at all?
At a dinner, an empty chair is ready to use. A chair with a "reserved" card is different. Before anyone sits there, the staff check the card and decide what to do.
- In real life: An empty chair anyone can sit on
- In JavaScript: The real
undefinedvalue - In real life: A "reserved" card on a chair
- In JavaScript: The hole, V8's own marker
- In real life: Staff check the card before anyone sits
- In JavaScript: V8 follows a rule before your code gets a result
Arrays with gaps
You can write an array with a gap: [1, , 3]. The two commas with nothing between them leave index 1 empty. An array with gaps is called a sparse array. V8 keeps the hole in the empty slot.
V8 also labels every array with an elements kind: what sort of items it holds, and whether it may have gaps. Two labels matter here:
HOLEY_SMI_ELEMENTS: small whole numbers, and there may be gaps. (A Smi is a small whole number.)PACKED_ELEMENTS: any values, and no gaps. Packed means every index has an item.
The Elements kinds lesson covers all the labels.
%DebugPrint([1, , 3]);%DebugPrint([1, undefined, 3]);In Node 22, line 1 prints HOLEY_SMI_ELEMENTS, and the empty slot shows as <the_hole_value>. Line 2 prints PACKED_ELEMENTS. In [1, undefined, 3], index 1 is a real item that holds undefined, so there is no gap.
const sparse = [1, , 3];const explicit = [1, undefined, 3]; console.log(1 in sparse, 1 in explicit);console.log(sparse[1], explicit[1]);console.log(Object.keys(sparse).join(","));console.log(Object.keys(explicit).join(","));sparse.forEach((value, index) => console.log("sparse", index, String(value)));explicit.forEach((value, index) => console.log("explicit", index, String(value)));Line 1 makes an array with a gap. Line 2 makes an array that holds undefined at index 1. Then:
- Line 4 prints
false true. Theinoperator asks "does this array have an item at this index?"sparsehas none at index 1;explicithas one. - Line 5 prints
undefined undefined. A plain read can't tell the two apart. - Lines 6 and 7 print
0,2and0,1,2.Object.keyslists the indexes that have items, so it skips the gap. - Lines 8 and 9:
forEachalso skips the gap. It prints two lines forsparseand three forexplicit.
undefined at index 1. But sparse has nothing there, a gap, while explicit really holds undefined.Why does reading a gap give undefined? Because a gap means "no item here", and JavaScript then looks somewhere else. Every array has a prototype: a shared object it falls back on, Array.prototype. When an array has no item at an index, JavaScript looks for that index in Array.prototype, then in Object.prototype. This line of fallback objects is called the prototype chain. Only if nothing is found there does the read give undefined.
const sparse = [1, , 3];const hadOwn = Object.prototype.hasOwnProperty.call(Array.prototype, 1);const oldValue = Array.prototype[1]; try { Array.prototype[1] = "surprise"; console.log(sparse[1]);} finally { if (hadOwn) { Array.prototype[1] = oldValue; } else { delete Array.prototype[1]; }}Line 1 makes the array with a gap. Lines 2 and 3 remember whether Array.prototype already had an item 1, and what it was. Normally it has none. Line 6 puts "surprise" at Array.prototype[1]. Line 7 reads sparse[1] and prints surprise: the gap found the item in the prototype.
A finally block always runs after try. Here, lines 9 to 13 put Array.prototype back the way it was, so later code on this page isn't affected.
sparse has no item 1, so JavaScript looks in Array.prototype next. The example put "surprise" there, so sparse[1] gives "surprise".Looking through the prototype chain for every gap would be slow. So V8 keeps a protector: a yes-or-no flag that says "nobody has put numbered items on Array.prototype or Object.prototype". While the flag is true, fast code can skip the prototype lookup. The flag is called the NoElements protector. Once someone adds a numbered item there, V8 sets the flag to false and never sets it back.
console.log(%NoElementsProtector());const hadOwn = Object.prototype.hasOwnProperty.call(Array.prototype, 1);const oldValue = Array.prototype[1];try { Array.prototype[1] = "prototype element"; console.log(%NoElementsProtector());} finally { if (hadOwn) Array.prototype[1] = oldValue; else delete Array.prototype[1];}%NoElementsProtector() is another special helper that needs node --allow-natives-syntax. Line 1 prints true. Line 5 adds an item at Array.prototype[1]. Line 6 prints false. Lines 8 and 9 put the prototype back. The name comes from V8's protectors.h, where NoElements becomes NoElementsProtector.
Reading a gap in an array. Your code never sees the hole itself. It sees the result of looking up index 1.
script
const explicit = [1, undefined, 3];console.log(1 in sparse);console.log(1 in explicit);console.log(sparse[1]);const hadOwn = Object.prototype.hasOwnProperty.call(Array.prototype, 1);const oldValue = Array.prototype[1];try { Array.prototype[1] = "surprise"; console.log(sparse[1]);} finally { if (hadOwn) Array.prototype[1] = oldValue; else delete Array.prototype[1];}Press Step to walk through the replay. It reads sparse[1] twice: once before the prototype gets an item 1, and once after. The first read gives undefined. The second gives "surprise".
Missing properties
Reading a property such as receiver.missing works in steps:
- V8 looks in the object itself. A property stored there is called an own property.
- If it isn't there, V8 looks in the object's prototype, then in that prototype's prototype, and so on.
- The chain ends at
null. If no object had the property, the read givesundefined.
A missing variable name is different. Reading a name that was never declared throws a ReferenceError.
const parent = { inherited: "from prototype" };const receiver = Object.create(parent);receiver.present = 1; console.log(receiver.present);console.log(receiver.inherited);console.log(receiver.missing);console.log("missing" in receiver);Line 1 makes a parent object with one property. Line 2 uses Object.create(parent) to make a new object whose prototype is parent. Line 3 gives the new object its own property, present.
- Line 5 prints
1, found in the object itself. - Line 6 prints
from prototype, found inparent. A property found this way is inherited. - Line 7 prints
undefined. No object in the chain hasmissing. - Line 8 prints
false. Theinoperator checks the whole chain and finds nothing.
V8 makes repeated property reads fast with an inline cache: a small memory at each property read in your code. It remembers what happened last time, so the next read can skip the search. What it saves is called a handler: a short recipe for the read. One handler is named LoadNonExistent. It means "this property doesn't exist, so give undefined".
// V8 12.4: src/ic/ic.ccHandle<Smi> smi_handler = LoadHandler::LoadNonExistent(isolate());handler = MaybeObjectHandle(LoadHandler::LoadFullChain( isolate(), lookup_start_object_map(), MaybeObjectHandle(isolate()->factory()->null_value()), smi_handler)); // V8 12.4: src/ic/accessor-assembler.cc// This is a handler for a load of a non-existent value.exit_point->Return(UndefinedConstant()); // V8 12.4: src/ic/handler-configuration.ccHandle<Object> validity_cell = Map::GetOrCreatePrototypeChainValidityCell( lookup_start_object_map, isolate);handler->set_validity_cell(*validity_cell);In plain words, the highlighted lines say:
- Lines 2 and 3: make a
LoadNonExistenthandler that covers the whole prototype chain. - Lines 8 and 9: this handler is for a missing property, and it gives back
undefined. - Lines 12 and 14: attach a validity cell, a flag that stays valid only while the prototype chain doesn't change.
The source names LoadHandler::LoadNonExistent, LoadFullChain, and a validity cell for the prototype chain. That supports one small claim: V8 can remember "this property is missing" and give undefined quickly. It does so only while the object's map is the same and the prototype chain hasn't changed. For the full story, see Inline caches and prototype internals.
The global undefined can't be changed
The name undefined is also a property of the global object, globalThis. Every property has a few settings. You can read them with Object.getOwnPropertyDescriptor. The result is called a descriptor. Three of its settings matter here:
writable: can you give the property a new value?enumerable: does it show up when you list an object's properties?configurable: can you delete it or change its settings?
For globalThis.undefined, all three are false. The language rules, ECMA-262, say so, and the lesson's test checks it.
const descriptor = Object.getOwnPropertyDescriptor(globalThis, "undefined");console.log(descriptor.writable, descriptor.enumerable, descriptor.configurable); (function sloppyAssignment() { undefined = "changed"; console.log(undefined);})(); (function strictAssignment() { "use strict"; try { undefined = "changed"; } catch (error) { console.log(error.name); }})();- Lines 1 and 2 read the descriptor and print
false false false. - Lines 5 and 6 run in normal, non-strict code. The assignment on line 5 is quietly ignored, so line 6 prints
undefined. - Line 10 turns on strict mode. Now the assignment on line 12 throws, and line 14 prints
TypeError.
You may see void 0 in older or generated code. In very old JavaScript, the global undefined could be changed, so careful code wrote void 0 instead. A minifier is a tool that makes code smaller, and some still write void 0 because it is shorter. Terser, a popular minifier, has an option for this called unsafe_undefined. But it isn't faster: the bytecode above is the same for void 0 and the usual undefined.
Playground: pick some code and read its bytecode
Pick one of the twelve pieces of code. The left side shows the JavaScript. The right side shows the names of its bytecode instructions, copied from real Node output. The lesson's test runs each piece again with Node's --print-bytecode --print-bytecode-filter options and checks that the names still match.
function retVoid() { return; }retVoid();00 LdaUndefined01 ReturnA bare `return;` loads `undefined` with `LdaUndefined`, then `Return` gives it back.
A bare return; loads undefined with LdaUndefined, then Return gives it back.
Practical use
Most everyday code doesn't need to think about oddballs. This lesson helps in three ways: you won't mix up a gap with a real undefined, you'll read performance tools more easily, and you'll write clearer code when undefined means something.
The table below turns the lesson into habits. Read each row as: "do this, like this, because of this."
| Habit | How to do it | Why it helps |
|---|---|---|
| Keep arrays packed | Write [value, undefined, value] when you want a real empty item, not a gap. | Arrays with gaps need extra checks, and a gap can show items from the prototype. |
Don't use new Array(n) without thinking in code that runs very often | Fill the array on purpose, or use Array.from, when every index should have an item. | new Array(n) starts full of gaps. Measure before you rewrite clear code. |
Don't make a variable named undefined | A local let undefined = ... is allowed, but it confuses readers. | V8 can no longer use its quick LdaUndefined instruction for that name. |
Use void 0 only for old code or minifiers, not for speed | Modern V8 makes the same LdaUndefined for undefined and void 0. | Write the clearer one, unless a build tool chooses void 0 for you. |
- When every index should have an item, keep the array packed. Use a real
undefineditem only when an empty item is the point. - In code that runs very often, don't make arrays with
new Array(n), which starts full of gaps, unless measuring shows it is fine. - Don't make local variables named
undefined. It is allowed, but it confuses readers and V8's quickLdaUndefinedpath. - Use
void 0only for old browsers or generated code. Modern V8 doesn't make it faster.
Common mix-ups
- "Every empty-looking slot is
undefined." No. Array gaps and TDZ variables hold the hole. V8 follows a rule before your code gets a result or an error. - "Reading a gap simply gives
undefined." Not straight away. JavaScript looks in the prototype chain first, so an item there can show up. - "
void 0is faster thanundefined." No. In the cases tested here, both become the sameLdaUndefinedinstruction. - "A missing property and an undeclared variable are the same." No. A missing property gives
undefined. An undeclared name throws aReferenceError. - "The addresses in Node's debug output are facts to memorize." No. Addresses and sizes change between builds. Type names and instruction names stay the same.
The table below compares five things that are easy to mix up. Read each row as: "when I have this, what is it, what does my code see, and what does V8 do?"
| When you have | What it is | What your code sees | What V8 does |
|---|---|---|---|
undefined | The real JavaScript value. | typeof gives "undefined", and === undefined is true. | Points to the one oddball in read-only memory. |
| An array gap | An index with no item. | A read gives undefined, but only after looking in the prototype chain. | Keeps the hole there, not the oddball. |
| A missing property | No object in the chain has this property. | The read gives undefined. | Can remember the miss with a LoadNonExistent handler. |
| A missing argument | The call passed fewer values than there are parameters. | The parameter is undefined inside the function. | Fills the empty parameter slots with undefined before the function starts. |
| A name that was never declared | No variable with that name exists. | Reading it throws a ReferenceError. | There is no value at all, so no undefined either. |
Practice exercises
6 EXERCISESEach exercise is a short program. The first ones ask for one word or one output. The last one asks you to pick a kind of array. Try a guess first, then use the hints if you get stuck.
This function returns undefined. Which bytecode instruction loads the real undefined for it?
function f() { return undefined; }
f();The instruction is LdaUndefined. It loads the one shared undefined into the accumulator. Then Return gives it back.
What does the first line of output, 1 in sparse, print?
const sparse = [1, , 3];
const explicit = [1, undefined, 3];
console.log(1 in sparse);
console.log(1 in explicit);The program prints false, then true. sparse has no item at index 1. explicit has one, even though it holds undefined.
The call passes one value to a function with two parameters. What does the program print?
function second(a, b) {
return b;
}
console.log(second("first"));It prints undefined. Before the function starts, V8 fills the missing parameter b with undefined.
Can you change the global undefined property? Run the code and write what it prints.
const descriptor = Object.getOwnPropertyDescriptor(globalThis, "undefined");
console.log(descriptor.writable);It prints false. You can't give the global undefined a new value.
After the code puts an item into Array.prototype, what does sparse[1] print?
const sparse = [1, , 3];
const hadOwn = Object.prototype.hasOwnProperty.call(Array.prototype, 1);
const oldValue = Array.prototype[1];
try {
Array.prototype[1] = "surprise";
console.log(sparse[1]);
} finally {
if (hadOwn) {
Array.prototype[1] = oldValue;
} else {
delete Array.prototype[1];
}
}It prints surprise. The gap has no item, so the read looks in Array.prototype and finds item 1. Then the finally block puts the prototype back.
Which word does V8 use for an array that has an item at every index?
const packed = Array.from({ length: 3 }, (_, index) => index + 1);
const holey = new Array(3);
console.log(packed.length, 1 in packed);
console.log(holey.length, 1 in holey);The word is packed. The code prints 3 true and 3 false: index 1 has an item in packed but not in holey. Use packed arrays when every index should have an item.
Quiz: check your understanding
8 QUESTIONSQuestion 1 of 8What is
undefinedinside V8?Choose an answer to see the explanation.
Question 2 of 8What does this program print?
Read the code, then predictconst a = undefined; const b = void 0; console.log(a === b);Choose an answer to see the explanation.
Question 3 of 8Node 22 printed the bytecode for
return undefinedandreturn void 0. Which two instructions did both give?Choose an answer to see the explanation.
Question 4 of 8What do these checks print?
Read the code, then predictconst sparse = [1, , 3]; const explicit = [1, undefined, 3]; console.log(1 in sparse); console.log(1 in explicit);Choose an answer to see the explanation.
Question 5 of 8This function reads
ybefore itsletline. What does its bytecode show?Read the code, then predictJavaScriptfunction tdz() { const r = y; let y = 1; return r; }Choose an answer to see the explanation.
Question 6 of 8The function has two parameters, but the call passes one value. What does it print?
Read the code, then predictfunction second(a, b) { return b; } console.log(second("first"));Choose an answer to see the explanation.
Question 7 of 8This non-strict function tries to change the global
undefined. What does it print?Read the code, then predict(function sloppyAssignment() { undefined = "changed"; console.log(undefined); })();Choose an answer to see the explanation.
Question 8 of 8Which habit follows from what you learned about gaps?
Choose an answer to see the explanation.
Key takeaways
- V8 has exactly one
undefined: a read-only oddball made at startup. Gettingundefinednever makes a new value. - The
LdaUndefinedinstruction loads it forreturn;,void 0, and the usual globalundefined. - The hole is a different marker, used for array gaps and the TDZ. Your code never gets the hole itself.
- Missing properties and missing arguments both give
undefined, but V8 gets there in different ways. - Keep arrays packed and code clear. Measure before you change code for the engine's sake.
One-line definition: undefined is V8's one read-only oddball. Holes and missing lookups are paths inside V8 that may end in that value.
Up next: null inside the engine.