Episode 1 — Fundamentals / 1.18 — Operators and Type System

1.18.f — Truthy and Falsy Values

In one sentence: JavaScript automatically converts values to booleans in conditional contexts — exactly 8 values are falsy (convert to false), and everything else is truthy (convert to true), which creates both powerful patterns and treacherous bugs when 0, "", or [] behave in unexpected ways.

Navigation: ← 1.18.e — typeof Operator · 1.18.g — Type Coercion →


1. The 8 falsy values (memorize this list)

ValueTypeBoolean() result
falseBooleanfalse
0Numberfalse
-0Numberfalse
0nBigIntfalse
"" (empty string)Stringfalse
nullNullfalse
undefinedUndefinedfalse
NaNNumberfalse
console.log(Boolean(false));      // false
console.log(Boolean(0));          // false
console.log(Boolean(-0));         // false
console.log(Boolean(0n));         // false
console.log(Boolean(""));         // false
console.log(Boolean(null));       // false
console.log(Boolean(undefined));  // false
console.log(Boolean(NaN));        // false

There are exactly 8 falsy values. Everything else in JavaScript is truthy.


2. Everything else is truthy

This includes many values that surprise beginners:

// Obvious truthy values
console.log(Boolean(true));        // true
console.log(Boolean(1));           // true
console.log(Boolean("hello"));    // true
console.log(Boolean(42));          // true

// SURPRISING truthy values
console.log(Boolean([]));          // true  (empty array!)
console.log(Boolean({}));          // true  (empty object!)
console.log(Boolean("0"));         // true  (string "0" is NOT 0!)
console.log(Boolean("false"));     // true  (string "false" is NOT false!)
console.log(Boolean(" "));         // true  (space is a character!)
console.log(Boolean(new Boolean(false)));  // true  (object wrapper!)
console.log(Boolean(-1));          // true  (any non-zero number)
console.log(Boolean(Infinity));    // true
console.log(Boolean(-Infinity));   // true
console.log(Boolean(new Date()));  // true  (any object)
console.log(Boolean(/regex/));     // true  (RegExp is an object)
console.log(Boolean(function(){})); // true

Quick reference: truthy surprises

ValueTruthy?Why it surprises
[] (empty array)YesPeople expect "empty" to be falsy
{} (empty object)YesSame as above
"0"YesIt is a non-empty string
"false"YesIt is a non-empty string
" " (whitespace)YesIt is a non-empty string
new Boolean(false)YesIt is an object (objects are always truthy)
new Number(0)YesSame — object wrapper
new String("")YesSame — object wrapper
-1YesAny number except 0, -0, NaN
InfinityYesIt is a number, not 0/NaN

3. Why [] is truthy but [] == false is true

This is one of the most confusing behaviors in JavaScript:

// [] is truthy
if ([]) {
  console.log("This runs!");  // runs — [] is truthy
}

// But [] == false is true (!)
console.log([] == false);  // true

Step-by-step explanation of [] == false

The abstract equality algorithm (==) applies these steps:

  1. Boolean vs anything: Convert false to 0 → now comparing [] == 0
  2. Object vs number: Convert [] to primitive → [].toString()"" → now comparing "" == 0
  3. String vs number: Convert "" to 0 → now comparing 0 == 0
  4. Same type: 0 === 0true
// The chain of conversions
console.log([].toString());        // ""
console.log(Number(""));           // 0
console.log(Number(false));        // 0
// So: [] → "" → 0 == false → 0 → 0 ... 0 == 0 → true

More coercion traps

console.log([] == false);         // true
console.log([] == 0);             // true
console.log([] == "");            // true
console.log([0] == false);        // true  ([0].toString() = "0" → 0)
console.log([""] == false);       // true  ([""].toString() = "" → 0)
console.log([[]] == false);       // true  ([[]].toString() = "" → 0)

// But with ===, none of these are true
console.log([] === false);        // false
console.log([] === 0);            // false
console.log([] === "");           // false

Lesson: Use === to avoid these traps entirely.


4. Boolean conversion methods

Boolean() constructor (without new)

console.log(Boolean(0));          // false
console.log(Boolean(""));         // false
console.log(Boolean("hello"));    // true
console.log(Boolean([]));         // true

Double negation !!

console.log(!!0);          // false
console.log(!!"");         // false
console.log(!!"hello");    // true
console.log(!!null);       // false
console.log(!!undefined);  // false
console.log(!!{});         // true
console.log(!![]);         // true

Which to use?

MethodProsCons
Boolean(x)Explicit, readableSlightly longer
!!xShort, common idiomLess obvious to beginners

Most style guides accept both. Boolean() is clearer; !! is more common in the wild.


5. Truthy/falsy in control flow

if statements

const name = "";
if (name) {
  console.log("Has name");
} else {
  console.log("No name");  // This runs (empty string is falsy)
}

const items = [];
if (items.length) {
  console.log("Has items");
} else {
  console.log("No items");  // This runs (0 is falsy)
}

while loops

let input = "hello";
while (input) {
  console.log(input);
  input = "";  // Loop ends because "" is falsy
}

&& and ||

const user = { name: "Alice" };

// && returns first falsy or last truthy
const greeting = user.name && `Hello, ${user.name}!`;
console.log(greeting);  // "Hello, Alice!"

// || returns first truthy or last falsy
const display = user.nickname || user.name || "Anonymous";
console.log(display);  // "Alice"

Ternary operator

const count = 0;
const message = count ? `${count} items` : "No items";
console.log(message);  // "No items" (0 is falsy)

// But what if count IS 0 and that's valid?
const message2 = count !== undefined ? `${count} items` : "No items";
console.log(message2);  // "0 items" (explicit check)

6. Common bugs from falsy values

Bug 1: Valid 0 treated as missing

function displayScore(score) {
  // BUG: score of 0 is treated as "no score"
  if (!score) {
    return "No score available";
  }
  return `Score: ${score}`;
}

console.log(displayScore(85));   // "Score: 85"
console.log(displayScore(0));    // "No score available" (WRONG!)
console.log(displayScore(null)); // "No score available" (correct)

// FIX: be explicit about what "missing" means
function displayScoreFixed(score) {
  if (score === null || score === undefined) {
    return "No score available";
  }
  // Or: if (score == null) — the one acceptable == null check
  return `Score: ${score}`;
}

console.log(displayScoreFixed(0));     // "Score: 0" (correct!)
console.log(displayScoreFixed(null));  // "No score available"

Bug 2: Valid empty string treated as missing

function setUsername(input) {
  // BUG: user intentionally clears their display name to ""
  const name = input || "Anonymous";
  return name;
}

console.log(setUsername("Alice"));  // "Alice"
console.log(setUsername(""));       // "Anonymous" (WRONG — user wanted empty!)
console.log(setUsername(null));     // "Anonymous" (correct)

// FIX: use ?? to only fall back for null/undefined
function setUsernameFixed(input) {
  const name = input ?? "Anonymous";
  return name;
}

console.log(setUsernameFixed(""));    // "" (correct — user's choice)
console.log(setUsernameFixed(null));  // "Anonymous" (correct)

Bug 3: false as a legitimate value

function getSetting(key, settings) {
  // BUG: false is a valid setting value
  const value = settings[key] || "default";
  return value;
}

const config = { darkMode: false, volume: 0, name: "" };
console.log(getSetting("darkMode", config));  // "default" (WRONG!)
console.log(getSetting("volume", config));    // "default" (WRONG!)
console.log(getSetting("name", config));      // "default" (WRONG!)

// FIX: use ??
function getSettingFixed(key, settings) {
  return settings[key] ?? "default";
}

console.log(getSettingFixed("darkMode", config));  // false (correct)
console.log(getSettingFixed("volume", config));    // 0 (correct)
console.log(getSettingFixed("name", config));      // "" (correct)

Bug 4: Array/object emptiness check

const items = [];

// WRONG: [] is truthy, so this always passes
if (items) {
  console.log("Has items");  // This ALWAYS runs for any array!
}

// CORRECT: check .length
if (items.length > 0) {
  console.log("Has items");
}

// For objects: check Object.keys
const obj = {};
if (Object.keys(obj).length > 0) {
  console.log("Has properties");
}

7. Real-world conditional traps with examples

API response handling

// API returns: { data: [], error: null, count: 0 }
const response = { data: [], error: null, count: 0 };

// TRAP: all these checks are wrong
if (!response.data) { /* won't run — [] is truthy */ }
if (!response.count) { /* runs — 0 is falsy, but count IS 0 */ }
if (!response.error) { /* runs — null is falsy (correct here) */ }

// BETTER
if (response.data.length === 0) { console.log("No data"); }
if (response.count === 0) { console.log("Zero count"); }
if (response.error === null) { console.log("No error"); }

Form validation

function validateForm(fields) {
  const errors = [];

  // TRAP: what if allowedAge is 0?
  if (!fields.age) {
    errors.push("Age is required");  // Bug if age is 0
  }

  // BETTER
  if (fields.age === undefined || fields.age === null) {
    errors.push("Age is required");
  }

  // Or with == null
  if (fields.age == null) {
    errors.push("Age is required");
  }

  return errors;
}

Environment variable parsing

// process.env values are always strings or undefined

const port = process.env.PORT || 3000;
// If PORT="0", this gives 3000 (but "0" is truthy, so this actually works!)
// If PORT="" (empty string), this gives 3000 (intended)

const debug = process.env.DEBUG || false;
// If DEBUG="false", debug becomes "false" (truthy string — bug!)

// BETTER: explicit parsing
const debugMode = process.env.DEBUG === "true";
const portNum = Number(process.env.PORT) || 3000;

8. Best practices: explicit comparisons vs truthy checks

When truthy checks are FINE

// Checking for null/undefined/missing when 0, "", false are not valid
if (user.name) { ... }          // name must be a non-empty string
if (callback) { callback(); }   // function or nothing
if (error) { handleError(error); } // error object or null

When you MUST be explicit

// When 0 is a valid value
if (count !== undefined) { ... }     // not: if (count)
if (count != null) { ... }          // null or undefined check

// When "" is a valid value
if (typeof name === "string") { ... }  // not: if (name)

// When false is a valid value
if (setting !== undefined) { ... }     // not: if (setting)

// When checking array emptiness
if (arr.length > 0) { ... }           // not: if (arr)

// When checking object emptiness
if (Object.keys(obj).length > 0) { ... }  // not: if (obj)

Decision guide

ScenarioUse truthy check?Preferred approach
String must be non-emptyYesif (str)
String can be empty (valid)Noif (str != null) or if (typeof str === "string")
Number must be non-zeroYesif (num)
Number can be 0 (valid)Noif (num != null)
Boolean can be false (valid)Noif (flag !== undefined)
Object/function existsYesif (obj) / if (fn)
Array has elementsNoif (arr.length > 0)

Key takeaways

  1. 8 falsy values: false, 0, -0, 0n, "", null, undefined, NaN — memorize them.
  2. Everything else is truthy, including [], {}, "0", "false", and object wrappers.
  3. [] is truthy, but [] == false is true — this is the == coercion algorithm, not truthiness.
  4. Use !!value or Boolean(value) for explicit boolean conversion.
  5. Common bugs: 0, "", and false as valid values being replaced by fallbacks when using || or if (!x).
  6. Use ?? instead of || when 0, "", or false are legitimate values.
  7. Check array emptiness with .length, not truthiness. Check object emptiness with Object.keys().length.
  8. When in doubt, use explicit comparisons (=== null, !== undefined, .length > 0).

Explain-It Challenge

Explain without notes:

  1. List all 8 falsy values in JavaScript.
  2. Why is [] truthy but [] == false evaluates to true? Walk through the coercion steps.
  3. Give an example where using if (!value) creates a bug because 0 is a valid value.
  4. What is the difference between !!x and Boolean(x)?
  5. When should you use value ?? "default" instead of value || "default"?

Navigation: ← 1.18.e — typeof Operator · 1.18.g — Type Coercion →