JavaScript (ES2026) Module 2 – Values, types and operators
Booleans, truthy and falsy values
Which JavaScript values count as false in a condition, what the logical operators || and && really return, and how to write defaults that keep a valid 0.
What you will learn
- List the eight falsy values and predict how any other value converts to a boolean
- Explain what !, || and && return and when their right-hand operand runs
- Use ?? for defaults that must keep 0, false and the empty string
- Write conditions that state what they test instead of leaning on truthiness
What you’ll learn
What you will learn
- List the eight falsy values and predict how any other value converts to a boolean
- Explain what !, || and && return and when their right-hand operand runs
- Use ?? for defaults that must keep 0, false and the empty string
- Write conditions that state what they test instead of leaning on truthiness
On this page
Every if, while and ? : asks a yes-or-no question, and JavaScript has two values for the answer: true and
false, the booleans. A condition does not have to be a boolean, though: JavaScript converts whatever value it is
given. The language defines exactly eight values that convert to false, called falsy: false, 0, -0, 0n,
the empty string '', null, undefined and NaN. Every other value is truthy, including the strings '0' and
'false', an empty array [] and an empty object {}. This lesson shows where the conversion happens, what !, ||
and && really give back, why || is the wrong way to supply a default for a number that may be 0, and how to write
conditions that say exactly what they test.
true, false and Boolean()
A comparison such as 3 > 2 produces a boolean, and typeof reports its type as 'boolean'. To turn any other value
into a boolean yourself, call Boolean(value), or write !!value: the first ! converts the value and negates it,
and the second negates it back.
// A comparison gives one of the two boolean values; Boolean() and !! turn any value into one.
console.log(3 > 2, typeof (3 > 2));
console.log(Boolean('hello'), Boolean(''));
console.log(!!'hello', !!'');
console.log(!0, !'text');
// new Boolean() makes an object, and every object counts as true, even this one.
const wrapped = new Boolean(false);
console.log(typeof wrapped);
if (wrapped) console.log('the if block ran for new Boolean(false)'); Output
true boolean true false true false true false object the if block ran for new Boolean(false)
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node boolean-basics.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The two forms always agree. Boolean(value) says in words what it does, which helps readers who are new to the
language; !!value is shorter and common in existing code. The last two lines show a trap: new Boolean(false) is not
a boolean but an object that wraps one, and every object is truthy, so the if block runs. Never create booleans
with new. Call Boolean() without it, as a plain function.
The eight falsy values
Whenever the language needs a boolean, it applies a conversion that the ECMAScript standard calls ToBoolean. Its rule fits in one table: these eight values are falsy, and every other value is truthy.
| Value | Type | Where it usually comes from |
|---|---|---|
false |
boolean | a comparison or a flag that is off |
0 and -0 |
number | a count, a price or a score of zero |
0n |
bigint | a BigInt counter or ID of zero |
'' |
string | an input field the user left empty |
null |
null | code that says “no object here” on purpose |
undefined |
undefined | a variable, property or argument that was never given a value |
NaN |
number | arithmetic or a conversion that failed, such as Number('abc') |
// Boolean(value) for sixteen values: the eight falsy ones first, then eight that often surprise people.
const values = [
['false', false],
['0', 0],
['-0', -0],
['0n', 0n],
["''", ''],
['null', null],
['undefined', undefined],
['NaN', NaN],
["'0'", '0'],
["'false'", 'false'],
["' '", ' '],
['[]', []],
['{}', {}],
['[0]', [0]],
['-1', -1],
['new Boolean(false)', new Boolean(false)],
];
for (const [label, value] of values) {
console.log(`${label.padEnd(20)}${Boolean(value)}`);
} Output
false false
0 false
-0 false
0n false
'' false
null false
undefined false
NaN false
'0' true
'false' true
' ' true
[] true
{} true
[0] true
-1 true
new Boolean(false) true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node truthy-table.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The second half of the output is where people go wrong:
'0','false'and' 'are strings with at least one character in them, so they are truthy. A condition never reads what a string says; it only checks whether the string is empty.[]and{}are objects, and every object is truthy, however little it holds. Developers who know Python should note the difference: there an empty list is false, in JavaScript an empty array is true. Ask an array whether it is empty withitems.length === 0.-1is truthy: among numbers, only zero (with either sign) andNaNare falsy, so negative numbers andInfinityare truthy.new Boolean(false)is an object, as the first example showed.
Web browsers add one more falsy value. document.all, a legacy list of every element on the page, behaves like
undefined in a condition. The HTML standard requires it, so that old scripts which tested if (document.all) to
recognise very old browsers take the modern path today, and the ECMAScript standard allows it through an internal
slot called [[IsHTMLDDA]] that no other object has. You will never need it, but it explains why some references
list nine falsy values.
Where JavaScript converts to a boolean
The conversion runs in these places:
- the condition of
if,while,do … whileandfor; - the condition of the conditional operator,
condition ? a : b; - the operand of
!; - the left-hand operand of
||and&&(in a chain such asa || b || c, every operand except the last); - callbacks whose results are used as yes-or-no answers, such as those of
filter,find,someandevery.
Comparisons are not on the list. The loose equality operator == has conversion rules of its own, and they turn a
boolean into a number before comparing, so “is this value truthy?” and “is this value == true?” are different
questions:
/* eslint-disable eqeqeq -- the loose comparisons with == are what this example shows */
// Every one of these values is truthy, so `if (value)` runs its block for all four.
// Comparing with == true is a different question, with different answers.
const values = ['0', [], 'yes', 2];
for (const value of values) {
const label = JSON.stringify(value).padEnd(6);
console.log(`${label} truthy: ${Boolean(value)} == true: ${value == true} == false: ${value == false}`);
} Output
"0" truthy: true == true: false == false: true [] truthy: true == true: false == false: true "yes" truthy: true == true: false == false: false 2 truthy: true == true: false == false: false
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node truthy-not-equal.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
All four values are truthy, and none of them is == true: true becomes the number 1 first, and '0', [], 'yes'
and 2 do not become 1. Two of them are even == false. So never compare a value with true or false using ==:
write if (done), or if (done === true) when the value must be exactly the boolean true. The comment on the first
line of the example is for ESLint, a widely used linter: its eqeqeq rule reports every ==, which is a good
default, and the comment marks these comparisons as deliberate.
Equality: ==, === and Object.is takes the rules of == apart step by step.
What ||, && and ! give back
a || b and a && b start by testing a, but they do not return the result of that test. They return one of their
two operands, unchanged:
a || breturnsawhenais truthy; otherwise it evaluatesband returns it.a && breturnsawhenais falsy; otherwise it evaluatesband returns it.
How ||, && and ?? choose their result
Text description of the diagram
Three small flowcharts, one above the other. Each starts by asking a question about the left operand, a.
- a || b asks whether a is truthy. If it is, the result is a and b is never evaluated. If it is not, JavaScript evaluates b and the result is b.
- a && b asks the same question with the answers swapped. If a is truthy, JavaScript evaluates b and the result is b. If a is falsy, the result is a and b is never evaluated.
- a ?? b asks whether a is null or undefined. If it is, JavaScript evaluates b and the result is b. For any other value of a, including 0, the empty string and false, the result is a and b is never evaluated.
In every case the result is one of the two operands as it is, not a value converted to true or false.
The right-hand operand is evaluated only when it can still change the result, which is called short-circuiting:
// || and && give back one of their operands, which is not always true or false.
const name = 'Asha';
const empty = '';
const zero = 0;
const five = 5;
const nothing = null;
console.log(name || 'Guest');
console.log(empty || 'Guest');
console.log(zero && 'not reached');
console.log(five && 'second');
console.log(nothing || undefined);
// The right-hand operand runs only when it can still change the result.
function lookUp() {
console.log('lookUp() ran');
return 42;
}
const cached = 7;
console.log(cached || lookUp());
console.log(cached && lookUp()); Output
Asha Guest 0 second undefined 7 lookUp() ran 42
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node and-or-values.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
empty || 'Guest' gives 'Guest' because the empty string is falsy. zero && 'not reached' gives 0, because a
falsy left side settles an && on its own. nothing || undefined shows that when every operand is falsy, || returns
the last one, so the result can be undefined rather than false. In the last two lines, cached || lookUp() never
calls lookUp, while cached && lookUp() has to. Code relies on this on purpose, as in user && user.name, and it
hides bugs when the skipped operand was a function call that someone expected to run every time.
! is the odd one out: it converts its operand and always returns true or false, never the operand itself.
Defaults: || also replaces 0
The everyday use of || is a default, as in name || 'Guest'. That works for text that must not be empty, and it
breaks as soon as a falsy value is a real answer:
// A booking form asks "How many extra bags?", and 0 is a real answer.
function extraBagsOr(form) {
return form.extraBags || 1; // meant: 1 when the field is missing
}
function extraBagsNullish(form) {
return form.extraBags ?? 1; // 1 only when the field is null or undefined
}
const forms = [{ extraBags: 2 }, { extraBags: 0 }, {}, { extraBags: null }];
console.log('form || ??');
for (const form of forms) {
console.log(`${JSON.stringify(form).padEnd(22)}${extraBagsOr(form)} ${extraBagsNullish(form)}`);
}
// A default parameter fills in undefined only: null and '' are passed through.
function greet(name = 'Guest') {
return `Hello, ${name}`;
}
console.log([greet(), greet(undefined), greet(null), greet('')]); Output
form || ??
{"extraBags":2} 2 2
{"extraBags":0} 1 0
{} 1 1
{"extraBags":null} 1 1
[ 'Hello, Guest', 'Hello, Guest', 'Hello, null', 'Hello, ' ]
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node defaults.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The traveller asked for no extra bags, and form.extraBags || 1 booked one anyway, because 0 is falsy. The
nullish coalescing operator ?? fixes it: a ?? b returns b only when a is null or undefined, so 0,
'' and false pass through as they are. Choose between them by asking which values mean “missing”:
||when every falsy value means “missing”, as with a display name that must not be empty;??when onlynullandundefineddo, as with counts, prices, booleans and text that may be empty.
The last line of the run shows a related rule. A default parameter, such as name = 'Guest', is used only when
the argument is undefined, whether it was left out or passed in explicitly; null and '' arrive unchanged.
null, undefined, optional chaining and ?? goes deeper into
?? and its assignment form ??=.
Conditions that say what they test
Truthiness keeps conditions short, and short is good when the shortcut means exactly what you want. It turns into a bug when it also catches a value you meant to keep:
// Temperatures from a sensor; null and undefined mean that it did not answer.
const readings = [21, 0, null, -3, undefined, 18];
// filter(Boolean) keeps the truthy values, so it drops the valid 0 as well.
console.log(readings.filter(Boolean));
// Say what you mean: keep every reading that is not null or undefined.
console.log(readings.filter((t) => t !== null && t !== undefined)); Output
[ 21, -3, 18 ] [ 21, 0, -3, 18 ]
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node keep-zero.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
filter(Boolean) is a well-known way to remove empty entries from a list, but 0 is falsy, so it also removed a
reading of 0 degrees, which is a perfectly good temperature. The second filter states the real rule. The same thinking
applies to every condition:
| To check whether | Write | What a bare if (value) would get wrong |
|---|---|---|
| an array has items | items.length > 0 |
[] is truthy, so an empty array passes |
| a text field holds text | text.trim() !== '' |
' ' is truthy, so spaces count as an answer |
| a number was given | count !== undefined |
0 is falsy, so a real zero counts as missing |
| a value exists at all | value !== null && value !== undefined |
0, '', false and NaN count as missing |
| an option is switched on | if (options.verbose) |
nothing, as long as the option really is a boolean |
Two habits help with the rest. Keep booleans boolean: a function called isReady or hasItems should return true
or false, so wrap its expression in Boolean() or write a comparison, rather than returning “something truthy”.
And when a negated condition grows hard to read, rewrite it with De Morgan’s laws: !(a && b) means the same as
!a || !b, and !(a || b) the same as !a && !b.
Exercise: a blank field or a real answer
Exercise · Easy · JavaScript
Tell a blank form field from a real answer
A form handler has to tell a field that was left blank from one that holds a real answer. Write isBlank(value), which returns true for:
nullandundefined;- a string that is empty or holds only white space (spaces, tabs, line breaks, non-breaking spaces);
- an empty array;
and false for every other value, including 0, false, NaN, the string '0' and an array with items in it. It must return the boolean itself, true or false, not some other truthy or falsy value.
isBlank(' ') // true
isBlank(0) // false: zero is a real answer
isBlank([]) // true
isBlank(['', '']) // false: the array holds two items
The starter code returns !value, which looks right at first sight. Run the sample tests to see which values it gets wrong, then fix it.
Starter code · blank.mjs
/**
* True when a form field counts as blank: null, undefined, a string that is empty or only white space, or an empty
* array. Every other value, 0, false and NaN included, is a real answer.
*/
export function isBlank(value) {
// Replace this line with your code.
return !value;
} The sample tests · blank.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { isBlank } from './blank.mjs';
test('null and undefined are blank', () => {
assert.equal(isBlank(null), true);
assert.equal(isBlank(undefined), true);
});
test('an empty string and white space only are blank', () => {
assert.equal(isBlank(''), true);
assert.equal(isBlank(' '), true);
assert.equal(isBlank('\t\n '), true);
});
test('non-breaking and other Unicode spaces count as white space', () => {
assert.equal(isBlank('\u00a0'), true);
assert.equal(isBlank('\u00a0\u2003\u00a0'), true);
});
test('a string with any text in it is not blank', () => {
assert.equal(isBlank('a'), false);
assert.equal(isBlank(' 0 '), false);
assert.equal(isBlank('false'), false);
});
test('zero, false and NaN are real answers', () => {
assert.equal(isBlank(0), false);
assert.equal(isBlank(-0), false);
assert.equal(isBlank(0n), false);
assert.equal(isBlank(false), false);
assert.equal(isBlank(NaN), false);
});
test('an empty array is blank', () => {
assert.equal(isBlank([]), true);
});
test('an array with items is not blank, whatever the items are', () => {
assert.equal(isBlank([0]), false);
assert.equal(isBlank(['', '']), false);
assert.equal(isBlank([null]), false);
});
test('objects and functions are not blank', () => {
assert.equal(isBlank({}), false);
assert.equal(isBlank(() => {}), false);
}); A hint
Check the cases one at a time, in this order: null and undefined first, then strings (trim() removes white space, non-breaking spaces included, from both ends), then arrays (Array.isArray and length). A value that reaches the end of the function is a real answer. !value cannot do the job on its own, because it is also true for 0, false and NaN.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Self-check
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Which values are falsy in JavaScript, and is an empty array one of them? Eight values are falsy: false, the
numbers 0 and -0, the BigInt 0n, the empty string, null, undefined and NaN. In a browser, the legacy
document.all object is falsy as well. Every other value is truthy, and that includes every object, so an empty array
and an empty object are truthy, and so are the strings '0', 'false' and ' '. To ask whether an array is empty,
test its length, as in items.length === 0. To ask whether a value is missing, compare it with null and undefined
rather than relying on truthiness, because 0, '' and false are often valid values.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
In a shopping cart, setting an item's quantity to 0 should remove it, but the item stays with a quantity of 1. The handler starts with const qty = input.qty || 1;. Explain the bug, fix it, and say how you would find and prevent the same pattern elsewhere in the code base.
-
Fresher to mid
A feature flag is read from an environment variable set to the text false, yet if (flags.newCheckout) switches the feature on for everyone. Why does this happen, and how would you read boolean settings so that it cannot happen again?
Key takeaways
- A condition converts any value to a boolean. The falsy values are
false,0,-0,0n,'',null,undefinedandNaN; everything else is truthy,[],{},'0'and'false'included. Boolean(value)and!!valueconvert a value yourself; never writenew Boolean().- Truthy is not the same as
== true: never compare a value withtrueorfalseusing==. ||and&&return one of their operands and skip the right-hand one when the left decides;!always returns a boolean.value || fallbackreplaces every falsy value,0and''included; usevalue ?? fallbackwhen onlynullandundefinedmean “missing”.- Write the condition you mean:
items.length > 0,text.trim() !== '',count !== undefined.
References
- ECMAScript 2026 Language Specification: ToBoolean (Ecma International)
- ECMAScript 2026 Language Specification: Binary Logical Operators (Ecma International)
- ECMAScript 2026 Language Specification: Logical NOT Operator (Ecma International)
- ECMAScript 2026 Language Specification: The [[IsHTMLDDA]] Internal Slot (Ecma International)
- ECMAScript 2026 Language Specification: String.prototype.trim and White Space (Ecma International)
- HTML Living Standard: the HTMLAllCollection interface (WHATWG)
- Falsy (MDN Web Docs)
- Nullish coalescing operator (??) (MDN Web Docs)
- Default parameters (MDN Web Docs)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress