cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Booleans inside the engine

See how V8 stores true and false as read-only oddballs, runs ToBoolean and branch bytecodes, and boxes Boolean objects.

By the end, you can
  • 01
    Point to the real booleansExplain why V8 stores true and false as two read-only oddball heap objects and why primitive booleans do not allocate.
  • 02
    Trace boolean decisionsConnect ToBoolean source checks, Ignition bytecodes, and comparison results to actual branches.
  • 03
    Separate primitives, bits, and wrappersDistinguish tagged oddball pointers, optimized compiler bits, and heap-allocated Boolean wrapper 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.

Definition

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(...).

Primitive booleans in normal JavaScriptPop out in the code editor (opens in a new tab)JavaScript
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.

Verified V8 source facts

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".

Node-only oddball proofJavaScript
%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.

Real-life analogyTwo light switch positions

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 #true oddball and one #false oddball
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.

Representations of true and false across engines
EngineInternal representationPrimary source used here
V8 12.4true 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.
JavaScriptCorefalse is the immediate bit pattern 0x06; true is 0x07 in JSCJSValue.h.WebKit JSCJSValue.h comments and constants ValueFalse / ValueTrue.
SpiderMonkeyA 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.

Array elements are ordinary valuesPop out in the code editor (opens in a new tab)JavaScript
const flags = [true, false, true];console.log(flags[0] === true);console.log(flags[1] === false);console.log(flags.length);
Node-only array layout proofJavaScript
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.

Trace boolean locals through a stack-frame model
Step 0 of 5Ready
Your turn: follow the blue line

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.

Running in
  1. script
Next: line 8
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function chooseBadge(score) {  const passed = score >= 70;  const needsReview = !passed;  const pair = [passed, needsReview];  return { passed, needsReview, firstSlot: pair[0] };} 
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.

Teaching model, not a live memory dump

Stack frame slots

score → 82passed → #trueneedsReview → #falsepair → Array object

Read-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.

Real-life analogyA checklist with fixed questions

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.

Playground: follow V8-style ToBoolean checks
Selected ToBoolean checkPop out in the code editor (opens in a new tab)JavaScript
const value = true;console.log(Boolean(value));if (value) {  console.log("truthy branch");} else {  console.log("falsy branch");}
Decisiontrue
Source-inspired steptrue oddball
V8 source noteBoolean oddball fast path
Boolean(value)true
Branchtruthy branch
Try it yourself

true: true oddball. The value already is V8's true oddball, so the boolean fast path returns true. Real Boolean(value) is true.

The order is a teaching model based on V8 12.4 source. The Boolean result is computed by real JavaScript for the selected value.
Two V8 source paths

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.

Functions used for bytecode probesJavaScript
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;}
Stored bytecode mnemonics from Node 22 / V8 12.4Text
branchIf:Ldar a0JumpIfToBooleanFalseReturn lessIf:Ldar a1TestLessThan a0JumpIfFalseReturn doubleBang:Ldar a0ToBooleanReturn notValue:Ldar a0ToBooleanLogicalNotReturn notLess:Ldar a1TestLessThan a0LogicalNotReturn andValue:Ldar a0JumpIfToBooleanFalseLdar a1Return strictTrue:LdaTrueTestReferenceEqual a0Return

The 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.

Replay an if (value) branch
Step 0 of 4Ready
Your turn: follow the blue line

Step through a branch as a teaching replay: load the value, apply ToBoolean, jump to the chosen return.

Running in
  1. script
Next: line 8
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function label(value) {  if (value) {    return "truthy path";  }  return "falsy path";} 
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Value passed to label

Changing the value starts a fresh replay. Watch which step decides the branch.

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.
Branch replay sourceJavaScript
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.

V8 compiler source namesText
MachineRepresentation::kBitRepresentationChanger::InsertChangeBitToTaggedRepresentationChanger::GetBitRepresentationFor / ChangeTaggedToBit

That 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.

Primitive boolean, optimized bit, and wrapper object
ThingWhat it isWhen you meet it
Boolean primitiveA tagged reference to the true or false oddball in V8.typeof true is "boolean"; strict equality is pointer identity for the oddball.
Optimized comparison bitA 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.

Primitive conversion vs wrapper allocationPop out in the code editor (opens in a new tab)JavaScript
const primitive = Boolean(false);const boxed = new Boolean(false); console.log(primitive, typeof primitive);console.log(Boolean(boxed), typeof boxed);console.log(boxed.valueOf());
Node-only Boolean wrapper proofJavaScript
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.

Dense flags as a labelled modelPop out in the code editor (opens in a new tab)JavaScript
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.

Oddball, ToBoolean check, or heap object?
  • `true`
  • `false`
  • `a < b` result
  • `""` in `if (value)`
  • `0n` in `if (value)`
  • `new Boolean(false)`
  • `[true, false]`
Try it yourself
0 of 7 correct

Sort each item by the layer this lesson uses to explain it.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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 like false in an if.” 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 EXERCISES
Exercise 1 · Warm-upComparison result is a primitive

Predict the two outputs in order.

Starter codePop out in the code editor (opens in a new tab)JavaScript
console.log(Boolean(3 < 5));
console.log((3 < 5) === true);

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

    Exercise 2 · Warm-upName the branch bytecode

    Which mnemonic decides the branch for if (x) in the lesson's bytecode listing?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function branchIf(x) {
      if (x) return "yes";
      return "no";
    }
    console.log(branchIf(0));

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

      Exercise 3 · PracticeSpot the wrapper trap

      What does Boolean(new Boolean(false)) print?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const primitive = Boolean(false);
      const boxed = new Boolean(false);
      
      console.log(primitive, typeof primitive);
      console.log(Boolean(boxed), typeof boxed);
      console.log(boxed.valueOf());

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

        Exercise 4 · PracticeMaterialize an optimized bit

        What V8 representation-change name did the lesson give for turning a compiler bit into a tagged boolean?

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

          Exercise 5 · PracticeChoose compact dense flags

          Which built-in typed array did the practical model use for dense 0/1 flags?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          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);

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

            Exercise 6 · ChallengeReview a Boolean wrapper

            A teammate stores flags with new Boolean(false). Should the code keep that pattern?

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

              Quiz: check your understanding

              8 QUESTIONS
              Booleans inside the engine quiz · 8 questionsScore: first tries count
              1. Question 1 of 8In V8 12.4, what is the primitive value true?

                Choose an answer to see the explanation.

              2. Question 2 of 8What does this primitive comparison snippet print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                console.log(Boolean(3 < 5));
                console.log((3 < 5) === true);

                Choose an answer to see the explanation.

              3. Question 3 of 8Which bytecode pair best describes if (a < b)?

                Choose an answer to see the explanation.

              4. Question 4 of 8What does this wrapper snippet print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const boxed = new Boolean(false);
                console.log(Boolean(boxed));
                console.log(boxed.valueOf());

                Choose an answer to see the explanation.

              5. Question 5 of 8Why does if (a < b) not need ToBoolean?

                Choose an answer to see the explanation.

              6. Question 6 of 8What does Boolean(0n) print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                console.log(Boolean(0n));

                Choose an answer to see the explanation.

              7. Question 7 of 8What does TurboFan's MachineRepresentation::kBit let optimized code do?

                Choose an answer to see the explanation.

              8. Question 8 of 8What is the safest practical rule?

                Choose an answer to see the explanation.

              Key takeaways

              • V8 12.4 stores primitive true and false as 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 JumpIfToBooleanFalse for general conditions and direct boolean tests after comparisons.
              • TurboFan can keep boolean-like values as kBit until 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.

              CompleteFrontend Clear concepts. Working examples.