Booleans inside the engine
See how V8 stores true and false as read-only oddballs, runs ToBoolean and branch bytecodes, and boxes Boolean objects.
- 01Find the real true and falseExplain why V8 keeps
trueandfalseas two shared values in read-only memory, and why writing a boolean never makes a new one. - 02Follow an if from value to jumpConnect the ToBoolean checks, the bytecode instructions, and comparison results to the branch the code takes.
- 03Tell three things apartTell apart a boolean value (a pointer to a shared value), a bit inside fast code, and a
Booleanwrapper object.
true and false are two shared values
You know true and false from everyday JavaScript. This lesson shows how a JavaScript engine stores them. 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 true and exactly one false. V8 makes them once, when it starts. V8 calls these special built-in values oddballs. A variable that holds a boolean only stores a pointer to one of them. A pointer is a memory address: it says where a value is. Writing true, writing false, or working out a < b never makes a new value.
This lesson builds on Tagged values, Smis & heap numbers. That lesson shows how V8 marks each stored value as either a small number or a pointer. The mark is called a tag, so a stored pointer is a tagged pointer. If you want the beginner rules for truthy and falsy values first, read Booleans & truthiness.
A primitive boolean is a plain true or false value, not an object. In V8, it is a tagged pointer to one of two fixed oddballs, #true or #false. Both live in read-only space: a part of memory that nothing is allowed to change. A Boolean wrapper object is something different. Only new Boolean(...) makes one.
const first = true;const second = 3 < 5;console.log(first, second);console.log(typeof first);console.log(Boolean("non-empty"));Line 1 stores the literal true. Line 2 stores the result of the comparison 3 < 5, which is also true. Line 3 prints both: true true. Line 4 prints boolean, because typeof tells you the type of a value. Line 5 turns a non-empty string into a boolean and prints true.
first and second both point to the same true. V8 made one true and one false when it started, and every boolean points to one of them.Where V8 keeps true and false
V8 is written in the C++ language, and its source code is public. So we can read how it builds true and false. The file oddball.h describes oddballs. Its comment says: “The Oddball describes objects null, undefined, true, and false.” So 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.to_string: the value as a string.type_of: whattypeofgives for the value.kind: a small number that says which oddball this is.
For booleans, the kinds are kFalse = 0 and kTrue = 1. The same file defines kNotBooleanMask = ~1: a number with every bit set except the last one. In oddball-inl.h, the check IsBoolean tests (kind & kNotBooleanMask) == 0. That test passes only when the kind is 0 or 1. So only false and true pass.
The file setup-heap-internal.cc builds both values when V8 starts. It makes one boolean_map and gives it to both true_value and false_value. A map is V8's record of a value's type and shape. The file also fills in the ready-made answers:
true: as a number1, as a string"true", and typeof"boolean".false: as a number0, as a string"false", and typeof"boolean".
%DebugPrint(true);%DebugPrint(false);%DebugPrint(1 < 2);%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: #truefortrue.[Oddball] in ReadOnlySpace: #falseforfalse.- The same map address for both values, within one run.
- No
undetectableflag on the boolean map. You'll meet this flag in the ToBoolean section.
Line 3 prints the result of 1 < 2. It is the same #true oddball.
Why read-only? V8's static roots article explains it. Core values such as undefined and true are roots: values the engine always needs. They live in the read-only part of the heap. (The heap is the memory where objects live.) V8 loads them from its startup snapshot, a saved copy of memory that V8 reads when it starts. The Startup performance lesson explains what snapshots cost and save.
The details differ between programs that use V8. Node 22 here uses V8 12.4 without pointer compression, a trick that stores pointers in fewer bytes. Chrome uses newer V8 versions with pointer compression and the sandbox, a fence that limits which memory V8 can touch. So addresses and memory layout differ. The idea of one shared, read-only true and false is the same.
A light switch has two fixed positions: on and off. Turning on the light doesn't build a new "on". It picks one of the two positions that are already there.
- In real life: A switch has two positions: on and off
- In JavaScript: V8 has one
#trueand one#false - In real life: Every room's switch uses the same two positions
- In JavaScript: Every variable, property, and array item points to one of the two
- In real life: The two positions are built into the switch
- In JavaScript: Read-only space means the two values never change
Other engines store booleans in other ways. Each row below is one engine. It says how that engine stores true and false, and which source file shows it.
| Engine | How it stores true and false | Where the source shows it |
|---|---|---|
| V8 12.4 (Node 22) | true and false are two oddball objects in read-only memory. Variables hold tagged pointers to them. | src/objects/oddball.h, setup-heap-internal.cc, and %DebugPrint in Node 22. |
| JavaScriptCore (Safari) | The slot holds the number itself, not a pointer: 0x06 means false and 0x07 means true. | The comments and the constants ValueFalse and ValueTrue in WebKit's JSCJSValue.h. |
| SpiderMonkey (Firefox) | A JS::Value has the type mark JSVAL_TYPE_BOOLEAN and holds 0 or 1. | Mozilla's js/public/Value.h defines the boolean type mark and setBoolean. |
In JavaScriptCore, Safari's engine, comments in JSCJSValue.h list False: 0x06 and True: 0x07. These are not pointers. The stored value itself is the number 0x06 or 0x07. In SpiderMonkey, Firefox's engine, Value.h defines the type mark JSVAL_TYPE_BOOLEAN. It stores the boolean as the number uint32_t(b): 0 or 1.
Where variables keep their booleans
When a function runs, V8 gives that call a stack frame: a block of memory for that one call. V8's interpreter is the part of V8 that runs your code one small step at a time. It keeps each local variable in a register, a numbered slot in the stack frame.
When a slot holds a boolean, it holds a pointer to #true or #false. Object properties and array items work the same way. Because there is only one true, === between two booleans can come down to one question: do both pointers point to the same place? V8 first checks that both values really are booleans.
const flags = [true, false, true];console.log(flags[0] === true);console.log(flags[1] === false);console.log(flags.length);Line 1 makes an array of three booleans. Lines 2 and 3 each print true: the items are the usual true and false. Line 4 prints 3, the length of the array.
const flags = [true, false];%DebugPrint(flags);In Node 22 (V8 12.4), %DebugPrint([true, false]) reports PACKED_ELEMENTS. That is V8's name for an array of any values with no gaps. V8 has no special kind of array just for booleans. The items print as <true> and <false>: pointers to the same two oddballs. For the bigger memory picture, see Stack and heap and Heap layout.
Step through a small function. Its variables and array items hold booleans. The box beside it is a simple model of the function's slots and the two shared values.
script
function chooseBadge(score) { const passed = score >= 70; const needsReview = !passed; const pair = [passed, needsReview]; return { passed, needsReview, firstSlot: pair[0] };} A simple model, not a real view of memory
The function's slots
score → 82passed → #trueneedsReview → #falsepair → Array objectThe two shared values
#true [Oddball]#false [Oddball]The function's slots, object properties, and array items all point to the same two shared values.
Press Step to walk through the replay. It runs the real code and shows the real values. The box beside it is a simple model of memory. It does not read real memory addresses from the engine.
How V8 turns any value into true or false
An if needs a yes-or-no answer. When you write if (value), JavaScript first turns value into true or false. The language rules call this step ToBoolean. The rules are a short table. These values become false:
undefinedandnull.false.- The numbers
+0,-0, andNaN. - BigInt zero,
0n. - The empty string,
"".
Almost every other value becomes true. One odd exception, document.all, comes up below. Values that become false are called falsy. The rest are called truthy. V8 follows this table, with quick checks for the most common values.
A door guard has a short checklist. The first questions catch the easy cases. Some answers need one more question. Anyone who gets past every question goes in.
- In real life: The first questions catch the easy cases
- In JavaScript:
false,null,undefined, the empty string, and zero - In real life: Some answers need one more question
- In JavaScript: Heap numbers and BigInts get an extra check
- In real life: Anyone left after every question goes in
- In JavaScript: Objects are always truthy
const value = true;console.log(Boolean(value));if (value) { console.log("truthy branch");} else { console.log("falsy branch");}true: the shared true. The value already is V8's shared true, so the check answers true straight away. Real JavaScript gives Boolean(value) = true.
V8's checks use two words from the value-representation lesson. A Smi is a small whole number stored right in the slot. A heap number is a number stored as its own object on the heap.
V8 has two versions of ToBoolean. Both follow the same table from the language rules.
- CodeStubAssembler::BranchIfToBooleanIsTrue builds the fast version that V8's own machine code uses. It checks, in order: Smis; fixed values such as
false,true, and the empty string; undetectable objects; heap numbers; BigInts. Anything left is truthy. - Object::BooleanValue is the C++ version. It checks, in order: a Smi that isn't zero; a boolean;
nullorundefined; WebAssembly's null, when that is turned on; undetectable objects; string length; heap numbers; BigInts. Anything left istrue.
An undetectable object has a special bit on its map, and V8 treats it as falsy. Only null, undefined, and objects like the old document.all have this bit. That is how document.all can be falsy even though it is an object. Node has an internal helper, %GetUndetectable(), that makes such an object. The lesson test uses it to check that the object is falsy. Treat document.all as an old browser exception. Don't copy the idea in your own code.
The instructions V8 runs for an if
V8 doesn't run your source text directly. First it turns each function into bytecode: a list of small, simple instructions. V8's interpreter, called Ignition, runs those instructions one at a time. Each instruction has a short name, called a mnemonic. For example, Ldar a0 loads the first argument, a0, so the next instruction can use it. Return ends the function.
For a decision, Ignition uses a jump: an instruction that skips to another place in the list. Which jump it uses depends on one question: is the value already true or false?
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;}Each function holds one pattern: if (x), if (a < b), !!x, !x, !(a < b), a && b, and x === true. Below is the bytecode Node printed for each one. Only the mnemonics are kept. Memory addresses and instruction positions change from run to run, so they are left out. a0 and a1 mean the first and second argument.
branchIf:Ldar a0JumpIfToBooleanFalseReturn lessIf:Ldar a1TestLessThan a0JumpIfFalseReturn doubleBang:Ldar a0ToBooleanReturn notValue:Ldar a0ToBooleanLogicalNotReturn notLess:Ldar a1TestLessThan a0LogicalNotReturn andValue:Ldar a0JumpIfToBooleanFalseLdar a1Return strictTrue:LdaTrueTestReferenceEqual a0ReturnThe key difference is if (x) versus if (a < b):
if (x):xcould be any value, so Ignition usesJumpIfToBooleanFalse. This one instruction turns the value intotrueorfalse, then jumps if it isfalse.if (a < b):TestLessThancompares the two values. Its result is alreadytrueorfalse. SoJumpIfFalsejumps without converting anything.
x could be any value, so if (x) first turns it into true or false. A comparison already gives true or false, so if (a < b) jumps straight away.The other functions follow the same idea:
!!xusesToBoolean: turn the value intotrueorfalse.!xusesToBooleanLogicalNot: turn the value into a boolean, then flip it.!(a < b)usesLogicalNotright after the comparison, because the result is already a boolean.a && busesJumpIfToBooleanFalse, the same jump asif (x).x === trueloadstruewithLdaTrue. ThenTestReferenceEqualchecks: do both pointers point to the same place? That is what this Node build prints.
Pick a value below, then step through label(value). The replay shows which check decides the branch.
Step through an if as a replay: read the value, turn it into true or false, then jump to the matching 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));Fast code can use a single bit
Bytecode always works with tagged values: pointers to #true and #false. For code that runs many times, V8 has a second compiler called TurboFan. It turns busy functions into fast machine code: instructions the computer's processor runs directly.
TurboFan can store a value in a smaller form. It calls each form a representation. The file machine-type.h defines MachineRepresentation::kBit: a single bit, 0 or 1. In representation-change.cc, ChangeBitToTagged turns a bit into a tagged boolean, and ChangeTaggedToBit goes the other way.
MachineRepresentation::kBitRepresentationChanger::InsertChangeBitToTaggedRepresentationChanger::GetBitRepresentationFor / ChangeTaggedToBitSo fast code can keep the result of a comparison as 0 or 1 in a CPU register. That is a tiny storage spot inside the processor, not the interpreter's register from earlier. Fast code only makes the real true or false pointer when your JavaScript needs to see the value, for example to store it or print it. This is how V8's compiler is designed. Your page can't see that register.
true or false only when your code needs to see the value.These three things are easy to mix up. Each row below says what one of them is and where you meet it.
| Thing | What it is | Where you meet it |
|---|---|---|
| A boolean value | A tagged pointer to the true or false oddball in V8. | Everywhere in normal code. typeof true is "boolean", and === checks whether both point to the same oddball. |
| A bit in fast code | A 0 or 1 inside V8's compiler, stored as MachineRepresentation::kBit. | Never directly. TurboFan can keep it in a CPU register until your code needs true or false. |
new Boolean(false) | A wrapper object on the heap, which V8 calls a JSPrimitiveWrapper. Inside, it points to the false oddball. | Only when code uses new Boolean. The object is truthy, so don't use it. |
Boolean objects made with new
Boolean(x) without new runs ToBoolean. It gives back true or false: one of the two shared values. new Boolean(x) is different. It makes a new wrapper object on the heap: an object that holds a boolean inside it. The wrapper is an object, and every object is truthy. So the wrapper is truthy even when it holds 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());Line 1 stores the plain value false. Line 2 makes a wrapper object around false. Line 4 prints false boolean. Line 5 prints true object: the wrapper is truthy, and its type is object. Line 6 calls valueOf(), which gives back the value inside the wrapper, so it prints false.
Boolean(false) gives the plain value false. new Boolean(false) gives an object with false inside, and every object is truthy.const boxed = new Boolean(false);%DebugPrint(boxed);%DebugPrint(Boolean(false));The lesson test runs this in Node and checks the parts that are the same on every run:
[JSPrimitiveWrapper], V8's name for a wrapper object.- A
value:field that points to<false>. [Oddball] in ReadOnlySpace: #falsefor the plain value on line 3.
Using this in real code
What does all this mean for everyday code?
- Don't try to speed up
!!valueversusBoolean(value). Both give the same result. Pick the one that is clearer to read. - Don't use
new Boolean. It makes a truthy object, and readers won't expect that. - For millions of yes-or-no flags, think about memory and measure it. A normal array stores a pointer for each item. A typed array or a bitset stores small numbers instead.
A typed array holds numbers of one fixed size. Each item in a Uint8Array takes one byte. A bitset packs 8 flags into each byte, one bit per flag.
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);Line 1 stores four flags as booleans. Line 2 stores the same flags as the numbers 1 and 0 in a Uint8Array. Line 4 counts the true flags: filter(Boolean) keeps only the truthy items, so it prints 3. Line 5 adds up the numbers and also prints 3. Line 6 prints model bytes 32 4.
Line 6 is only a rough model. It counts 8 bytes per array item, the size of one pointer here, so 4 items make 32 bytes. packed.byteLength is the real size of the typed array's data: 4 bytes. Real arrays also need a header, storage for their items, and extra space that differs between engines. Pointer compression changes the sizes too. Before you change real code, measure with heap snapshots or speed tests.
`true``false``a < b` result`""` in `if (value)``0n` in `if (value)``new Boolean(false)``[true, false]`
Sort each item by the part of this lesson that explains it.
Common misconceptions
- “A boolean is a wrapper object.” No. A boolean is a pointer to one of the two shared oddballs. Only
new Booleanmakes a wrapper object. - “Every
ifruns the same slow conversion.” No. A comparison already gives a boolean, so the bytecode can useJumpIfFalsewith no conversion. - “Fast code's 0/1 bits are numbers your code can see.” No. They exist only inside the compiler. When your code reads the value, V8 turns it into
trueorfalse. - “
new Boolean(false)acts likefalsein anif.” No. It is an object, so it is truthy. - “Node and Chrome store values in exactly the same way.” No. Node 22 here has pointer compression turned off. Chrome uses a newer V8 with pointer compression and the sandbox.
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 gives the plain boolean true. Boolean() keeps it as it is, and === true matches it.
In the lesson's bytecode list, which instruction decides the branch for if (x)?
function branchIf(x) {
if (x) return "yes";
return "no";
}
console.log(branchIf(0));It is JumpIfToBooleanFalse. It turns the value into true or false, then jumps if the result is false.
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 is an object. boxed.valueOf() prints false because that is the value stored inside.
Which name from V8's compiler source turns a fast-code bit into a tagged boolean?
The name is ChangeBitToTagged. It turns a 0/1 bit from fast code into the tagged true or false value.
Which built-in typed array did the flags example use to store 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: one byte per flag. Use a bitset only when you need one bit per flag and you have measured that it matters.
A teammate stores flags as new Boolean(false). Should the code keep doing that?
No. new Boolean(false) makes a truthy object, so an if would treat the flag as on. Use plain true and false instead.
Quiz: check your understanding
8 QUESTIONSQuestion 1 of 8In V8 12.4, what is the boolean value
true?Choose an answer to see the explanation.
Question 2 of 8What does this comparison code 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 pair of bytecode instructions runs for
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)skip the ToBoolean step?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 fast code do?Choose an answer to see the explanation.
Question 8 of 8Which rule is best for real code?
Choose an answer to see the explanation.
Key takeaways
- In V8 12.4,
trueandfalseare two shared oddballs in read-only memory. - Variables, object properties, and array items hold pointers to those two values.
- ToBoolean comes from the language rules. V8 runs it with quick checks for Smis, fixed values, strings, heap numbers, and BigInts.
- Ignition uses
JumpIfToBooleanFalsefor a generalif (x), and a plainJumpIfFalseafter a comparison. - TurboFan can keep a boolean as a single bit (
kBit) until your code needs the realtrueorfalse. new Boolean(false)makes a truthy object. Don't use it in your code.
One-liner: V8 has one true and one false; every boolean points to one of them, and conversions and jumps decide which one.
Up next: undefined inside the engine. undefined is another oddball. You'll meet gaps in arrays, the check for using a variable too early, missing properties, and the LdaUndefined instruction.