Booleans inside the engine
See how V8 stores true and false as read-only oddballs, runs ToBoolean and branch bytecodes, and boxes Boolean objects.
- 01Point to the real booleansExplain why V8 stores
trueandfalseas two read-only oddball heap objects and why primitive booleans do not allocate. - 02Trace boolean decisionsConnect ToBoolean source checks, Ignition bytecodes, and comparison results to actual branches.
- 03Separate primitives, bits, and wrappersDistinguish tagged oddball pointers, optimized compiler bits, and heap-allocated
Booleanwrapper objects.
Booleans are oddball roots
In JavaScript you see two primitive boolean values: true and false. Inside V8 12.4, those two values are two pre-built heap objects called oddballs. A variable that holds a boolean carries a tagged reference to one of those roots. Writing true, writing false, or computing a < b does not allocate a public Boolean object.
This lesson builds directly on Tagged values, Smis & heap numbers. If you want the beginner truthy/falsy rules first, read Booleans & truthiness.
A V8 primitive boolean is a tagged reference to one of two immutable oddball objects, #true or #false, stored in read-only space. A Boolean wrapper is a different heap object created only by new Boolean(...).
const first = true;const second = 3 < 5;console.log(first, second);console.log(typeof first);console.log(Boolean("non-empty"));Line 1 uses a literal. Line 2 computes a comparison. Both print as normal primitive booleans on line 3, and typeof on line 4 stays "boolean".
true and false in read-only space
V8's oddball header says: “The Oddball describes objects null, undefined, true, and false.” It lists cached fields for to_number_raw, to_string, to_number, type_of, and kind. The same file defines kFalse = 0, kTrue = 1, and kNotBooleanMask = ~1. In oddball-inl.h, IsBoolean checks (kind & kNotBooleanMask) == 0, so only kinds 0 and 1 pass.
setup-heap-internal.cc creates one boolean_map, assigns it to both true_value and false_value, and initializes cached conversions: true has ToNumber 1, ToString "true", and typeof "boolean"; false has 0, "false", and "boolean".
%DebugPrint(true);%DebugPrint(false);%DebugPrint(1 < 2);The test runs that snippet with node --allow-natives-syntax. It asserts the stable pieces: [Oddball] in ReadOnlySpace: #true, [Oddball] in ReadOnlySpace: #false, the same map address within one run, and no undetectable flag on the boolean map. A comparison result such as 1 < 2 prints as the same #true oddball.
V8's static roots article explains why roots such as undefined and true live in the read-only heap and are loaded from the startup snapshot. For the startup cost side of snapshots, connect this to Startup performance. Node 22 here uses V8 12.4 without pointer compression; Chrome has newer V8 builds with pointer compression and the sandbox, so addresses and layout details differ. The read-only root idea is the shared design.
A light switch has two fixed positions: on and off. JavaScript code can refer to those shared choices without making a new one each time.
- In real life: A switch is on or off
- In JavaScript: V8 keeps one
#trueoddball and one#falseoddball - In real life: Rooms use the same two positions
- In JavaScript: Stack slots and fields hold tagged references
- In real life: The positions do not change
- In JavaScript: Read-only space prevents mutation
Where the analogy stops: A switch is physical. V8 roots are managed by the engine and baked into the startup snapshot.
| Engine | Internal representation | Primary source used here |
|---|---|---|
| V8 12.4 | true and false are oddball heap objects in read-only space; variables hold tagged references to them. | src/objects/oddball.h, setup-heap-internal.cc, and %DebugPrint in Node 22. |
| JavaScriptCore | false is the immediate bit pattern 0x06; true is 0x07 in JSCJSValue.h. | WebKit JSCJSValue.h comments and constants ValueFalse / ValueTrue. |
| SpiderMonkey | A boxed JS::Value uses JSVAL_TYPE_BOOLEAN with payload 0 or 1. | Mozilla js/public/Value.h defines the boolean type tag and setBoolean. |
For contrast, JavaScriptCore's JSCJSValue.h comments list False: 0x06 and True: 0x07. Mozilla's Value.h defines JSVAL_TYPE_BOOLEAN and sets a boolean payload with uint32_t(b).
Stack slots, registers, and heap pointers
The interpreter stores locals in register slots inside a stack frame. When a slot holds a primitive boolean, the value is a tagged reference to #true or #false. Object fields and array elements work the same way. Strict equality between primitive booleans can be the same-root comparison after type checks.
const flags = [true, false, true];console.log(flags[0] === true);console.log(flags[1] === false);console.log(flags.length);const flags = [true, false];%DebugPrint(flags);In Node 22/V8 12.4, %DebugPrint([true, false]) reports PACKED_ELEMENTS, not a special boolean elements kind. The elements print as <true> and <false>. Link this model back to Stack and heap and Heap layout for the broader memory picture.
Replay a tiny function whose local variables and array elements hold boolean values. The side diagram is a labelled model of stack slots and the read-only heap.
script
function chooseBadge(score) { const passed = score >= 70; const needsReview = !passed; const pair = [passed, needsReview]; return { passed, needsReview, firstSlot: pair[0] };} Teaching model, not a live memory dump
Stack frame slots
score → 82passed → #trueneedsReview → #falsepair → Array objectRead-only heap roots
#true [Oddball]#false [Oddball]Interpreter registers, object fields, and array elements can all carry tagged references to the same two roots.
The replay records real JavaScript values. The side memory pane is deliberately labelled as a teaching model; it is not an engine debugger and does not inspect raw addresses.
ToBoolean inside V8
ECMAScript defines ToBoolean as a table: undefined, null, false, +0, -0, NaN, 0n, and the empty string are false; most other values are true. V8 then implements fast source-level paths for that table.
Think of ToBoolean as a checklist with fixed questions. Simple false cases stop early, special values get another check, and ordinary objects pass.
- In real life: Early questions rule out simple cases
- In JavaScript: false, nullish, empty string, zero-like values
- In real life: Special answers get another question
- In JavaScript: HeapNumber and BigInt checks
- In real life: Everything else passes
- In JavaScript: ordinary objects are truthy
Where the analogy stops: People can change a checklist. V8's checks are compiled source code and must match ECMAScript exactly.
const value = true;console.log(Boolean(value));if (value) { console.log("truthy branch");} else { console.log("falsy branch");}true: true oddball. The value already is V8's true oddball, so the boolean fast path returns true. Real Boolean(value) is true.
CodeStubAssembler::BranchIfToBooleanIsTrue checks Smis, static read-only roots such as false/true/empty string, undetectable maps, HeapNumbers, BigInts, then ordinary truthy values. Object::BooleanValue has the runtime order: Smi non-zero, Boolean, null/undefined, WebAssembly null when enabled, undetectable, string length, HeapNumber, BigInt, then true. Both implement the same spec table.
The undetectable map bit is why V8 can make document.all-style legacy objects falsey; Node's internal %GetUndetectable() probe verifies the behavior, but regular application code should treat document.all as a web-compatibility exception, not a pattern to copy.
Ignition bytecode for boolean decisions
V8's interpreter, Ignition, uses different bytecodes depending on whether a value still needs ToBoolean or is already a boolean-like comparison result. The lesson stores mnemonic-only listings because bytecode offsets and addresses are not stable.
function branchIf(x) { if (x) return "yes"; return "no";} function lessIf(a, b) { if (a < b) return "lt"; return "ge";} function doubleBang(x) { return !!x;} function notValue(x) { return !x;} function notLess(a, b) { return !(a < b);} function andValue(a, b) { return a && b;} function strictTrue(x) { return x === true;}branchIf:Ldar a0JumpIfToBooleanFalseReturn lessIf:Ldar a1TestLessThan a0JumpIfFalseReturn doubleBang:Ldar a0ToBooleanReturn notValue:Ldar a0ToBooleanLogicalNotReturn notLess:Ldar a1TestLessThan a0LogicalNotReturn andValue:Ldar a0JumpIfToBooleanFalseLdar a1Return strictTrue:LdaTrueTestReferenceEqual a0ReturnThe important contrast is if (x) versus if (a < b). A general value uses JumpIfToBooleanFalse. A comparison uses TestLessThan and then JumpIfFalse because the comparison result is already one of the boolean values. !!x uses ToBoolean; !x uses ToBooleanLogicalNot; !(a < b) uses LogicalNot after the comparison; a && b uses JumpIfToBooleanFalse; and x === true loads true then runs TestReferenceEqual in this Node build.
Step through a branch as a teaching replay: load the value, apply ToBoolean, jump to the chosen return.
script
function label(value) { if (value) { return "truthy path"; } return "falsy path";} function label(value) { if (value) { return "truthy path"; } return "falsy path";} console.log(label(0));Optimized code can use one bit
Interpreter bytecode still deals in tagged values. TurboFan, V8's optimizing compiler, can choose lower-level machine representations. machine-type.h defines MachineRepresentation::kBit. In representation-change.cc, ChangeBitToTagged and ChangeTaggedToBit appear in the paths that cross between a compiler bit and a tagged boolean.
MachineRepresentation::kBitRepresentationChanger::InsertChangeBitToTaggedRepresentationChanger::GetBitRepresentationFor / ChangeTaggedToBitThat means optimized code can keep a comparison result as a 0/1 value in a CPU register and materialize the tagged true or false pointer only when the value escapes to observable JavaScript. This is a compiler design fact; the page cannot observe that register directly.
| Thing | What it is | When you meet it |
|---|---|---|
| Boolean primitive | A tagged reference to the true or false oddball in V8. | typeof true is "boolean"; strict equality is pointer identity for the oddball. |
| Optimized comparison bit | A compiler-internal 0/1 value with MachineRepresentation::kBit. | TurboFan can keep it in a register until it must materialize true or false. |
new Boolean(false) | A JSPrimitiveWrapper heap object whose internal value points at the false oddball. | The object itself is truthy, so avoid it in application code. |
Boolean wrapper objects
Boolean(x) without new returns the primitive oddball result of ToBoolean. new Boolean(x) allocates a wrapper object on the heap. That wrapper has an internal primitive value, but the wrapper itself is an object, so it is truthy even when it wraps false.
const primitive = Boolean(false);const boxed = new Boolean(false); console.log(primitive, typeof primitive);console.log(Boolean(boxed), typeof boxed);console.log(boxed.valueOf());const boxed = new Boolean(false);%DebugPrint(boxed);%DebugPrint(Boolean(false));The Node probe asserts stable text: [JSPrimitiveWrapper], a value: field pointing at <false>, and a later [Oddball] in ReadOnlySpace: #false for the primitive result.
Practical use
Do not micro-optimize !!value versus Boolean(value). Pick the form that communicates intent. Do avoid new Boolean; it creates a truthy object and surprises readers. For millions of dense flags, model memory honestly and measure: a normal array stores references to values, while a typed array or bitset stores compact numeric flags.
const featureFlags = [true, false, true, true];const packed = new Uint8Array([1, 0, 1, 1]); console.log(featureFlags.filter(Boolean).length);console.log(packed.reduce((sum, bit) => sum + bit, 0));console.log("model bytes", featureFlags.length * 8, packed.byteLength);The model bytes line is intentionally labelled: it uses eight bytes per array slot as a simplified reference-size model and compares that with Uint8Array#byteLength. Real arrays have headers, elements backing stores, pointer compression differences, and engine-specific overhead, so use heap snapshots or benchmarks for production decisions.
`true``false``a < b` result`""` in `if (value)``0n` in `if (value)``new Boolean(false)``[true, false]`
Sort each item by the layer this lesson uses to explain it.
Common misconceptions
- “A primitive boolean is a wrapper object.” No. V8 primitives point at oddballs; wrappers are allocated by
new Boolean. - “Every condition calls the same slow conversion.” No. Comparisons already produce booleans, so bytecode can use
JumpIfFalse. - “Optimized 0/1 bits are observable JavaScript numbers.” No. They are compiler internals that must materialize as tagged booleans when observed.
- “
new Boolean(false)behaves likefalsein anif.” No. It is an object, so it is truthy. - “Node and Chrome expose the same internal layout.” No. Node 22 here has pointer compression off; Chrome uses newer V8 with pointer compression and sandboxing.
Practice exercises
6 EXERCISESPredict the two outputs in order.
console.log(Boolean(3 < 5));
console.log((3 < 5) === true);Both lines print true. The comparison result is the primitive true value.
Which mnemonic decides the branch for if (x) in the lesson's bytecode listing?
function branchIf(x) {
if (x) return "yes";
return "no";
}
console.log(branchIf(0));The mnemonic is JumpIfToBooleanFalse. It converts a general value for the branch decision.
What does Boolean(new Boolean(false)) print?
const primitive = Boolean(false);
const boxed = new Boolean(false);
console.log(primitive, typeof primitive);
console.log(Boolean(boxed), typeof boxed);
console.log(boxed.valueOf());const primitive = Boolean(false);
const boxed = new Boolean(false);
console.log(primitive, typeof primitive);
console.log(Boolean(boxed), typeof boxed);
console.log(boxed.valueOf());Boolean(boxed) prints true because the wrapper object exists. boxed.valueOf() prints false because that is the wrapped primitive.
What V8 representation-change name did the lesson give for turning a compiler bit into a tagged boolean?
The operator name is ChangeBitToTagged: it materializes a compiler bit as a tagged boolean value.
Which built-in typed array did the practical model use for dense 0/1 flags?
const featureFlags = [true, false, true, true];
const packed = new Uint8Array([1, 0, 1, 1]);
console.log(featureFlags.filter(Boolean).length);
console.log(packed.reduce((sum, bit) => sum + bit, 0));
console.log("model bytes", featureFlags.length * 8, packed.byteLength);Use a Uint8Array as the simple built-in compact model, or a bitset when you need one bit per flag and have measured that it matters.
A teammate stores flags with new Boolean(false). Should the code keep that pattern?
No. Avoid new Boolean(false) for flags because it creates a truthy object. Use primitive booleans instead.
Quiz: check your understanding
8 QUESTIONSQuestion 1 of 8In V8 12.4, what is the primitive value
true?Choose an answer to see the explanation.
Question 2 of 8What does this primitive comparison snippet print?
Read the code, then predictconsole.log(Boolean(3 < 5)); console.log((3 < 5) === true);Choose an answer to see the explanation.
Question 3 of 8Which bytecode pair best describes
if (a < b)?Choose an answer to see the explanation.
Question 4 of 8What does this wrapper snippet print?
Read the code, then predictconst boxed = new Boolean(false); console.log(Boolean(boxed)); console.log(boxed.valueOf());Choose an answer to see the explanation.
Question 5 of 8Why does
if (a < b)not need ToBoolean?Choose an answer to see the explanation.
Question 6 of 8What does
Boolean(0n)print?Read the code, then predictconsole.log(Boolean(0n));Choose an answer to see the explanation.
Question 7 of 8What does TurboFan's
MachineRepresentation::kBitlet optimized code do?Choose an answer to see the explanation.
Question 8 of 8What is the safest practical rule?
Choose an answer to see the explanation.
Key takeaways
- V8 12.4 stores primitive
trueandfalseas read-only oddball roots. - Stack slots, registers, object fields, and array elements hold tagged references to those roots.
- ToBoolean is specified by ECMAScript and implemented in V8 with fast paths for Smis, roots, maps, strings, HeapNumbers, and BigInts.
- Ignition uses
JumpIfToBooleanFalsefor general conditions and direct boolean tests after comparisons. - TurboFan can keep boolean-like values as
kBituntil it must materialize a tagged boolean. new Boolean(false)allocates a truthy wrapper object; avoid it in application code.
One-liner: Primitive booleans are two shared engine roots; conversions and bytecodes decide when code points at #true or #false.
Up next: undefined inside the engine, where another oddball meets holes, TDZ checks, missing properties, and LdaUndefined.