Episode 1 — Fundamentals / 1.18 — Operators and Type System
1.18.c — Logical Operators
In one sentence: JavaScript's logical operators (
&&,||,!,??) do not simply returntrue/false— they evaluate using short-circuit logic and return the actual operand value that determined the result, which enables powerful patterns for defaults, guards, and conditional assignment.
Navigation: ← 1.18.b — Comparison Operators · 1.18.d — Assignment Operators →
1. Logical AND &&
&& returns the first falsy operand it encounters, or the last operand if all are truthy. It does not always return a boolean.
console.log(true && "hello"); // "hello" (both truthy → returns last)
console.log(false && "hello"); // false (first is falsy → returns it)
console.log("a" && "b" && "c"); // "c" (all truthy → last value)
console.log("a" && 0 && "c"); // 0 (first falsy encountered)
console.log("a" && "" && "c"); // "" (empty string is falsy)
console.log(1 && 2 && 3); // 3
console.log(1 && 0 && 3); // 0
Short-circuit evaluation
&& stops evaluating as soon as it finds a falsy value. The right operand is never executed if the left is falsy.
let count = 0;
false && count++; // count stays 0 (right side never runs)
console.log(count); // 0
true && count++; // count becomes 1
console.log(count); // 1
Guard pattern (pre-optional-chaining)
// Before optional chaining (?.)
const street = user && user.address && user.address.street;
// Modern equivalent
const street2 = user?.address?.street;
Conditional rendering (React pattern)
// In JSX: only renders <Welcome /> if isLoggedIn is truthy
// {isLoggedIn && <Welcome />}
const isLoggedIn = true;
const message = isLoggedIn && "Welcome back!";
console.log(message); // "Welcome back!"
2. Logical OR ||
|| returns the first truthy operand it encounters, or the last operand if all are falsy.
console.log(false || "hello"); // "hello" (first truthy)
console.log("" || "default"); // "default"
console.log(0 || 42); // 42
console.log(null || undefined); // undefined (both falsy → last)
console.log("a" || "b"); // "a" (first is truthy → returns it)
console.log(false || 0 || "" || "found"); // "found"
console.log(false || 0 || "" || null); // null (all falsy → last)
Short-circuit evaluation
|| stops evaluating as soon as it finds a truthy value.
let init = false;
true || (init = true); // init stays false (right side never runs)
console.log(init); // false
false || (init = true); // init becomes true
console.log(init); // true
Default value pattern
function greet(name) {
const displayName = name || "Guest";
return `Hello, ${displayName}!`;
}
console.log(greet("Alice")); // "Hello, Alice!"
console.log(greet("")); // "Hello, Guest!" (problem! "" is falsy)
console.log(greet(0)); // "Hello, Guest!" (problem! 0 is falsy)
console.log(greet(undefined)); // "Hello, Guest!" (intended)
Problem: || treats 0, "", and false as falsy — if those are valid values, || will incorrectly replace them. This is why ?? was introduced.
3. Logical NOT !
! converts the operand to a boolean and inverts it.
console.log(!true); // false
console.log(!false); // true
console.log(!"hello"); // false ("hello" is truthy)
console.log(!""); // true ("" is falsy)
console.log(!0); // true
console.log(!null); // true
console.log(!undefined); // true
console.log(!NaN); // true
Double negation !! — explicit boolean conversion
!!value is a common idiom to convert any value to its boolean equivalent:
console.log(!!"hello"); // true
console.log(!!""); // false
console.log(!!0); // false
console.log(!!1); // true
console.log(!!null); // false
console.log(!!undefined); // false
console.log(!!{}); // true (objects are truthy)
console.log(!![]); // true (arrays are truthy)
Equivalent to Boolean(value), but shorter. Some style guides prefer Boolean() for readability.
// Both do the same thing
const hasName = !!user.name;
const hasName2 = Boolean(user.name);
4. Nullish coalescing ?? (ES2020)
?? returns the right operand only if the left is null or undefined. It does NOT treat 0, "", or false as "missing."
console.log(null ?? "default"); // "default"
console.log(undefined ?? "default"); // "default"
console.log(0 ?? "default"); // 0 (0 is NOT null/undefined)
console.log("" ?? "default"); // "" ("" is NOT null/undefined)
console.log(false ?? "default"); // false (false is NOT null/undefined)
?? vs || — the critical difference
| Expression | || result | ?? result |
|---|---|---|
0 || 42 | 42 | 0 |
"" || "default" | "default" | "" |
false || true | true | false |
null || "fallback" | "fallback" | "fallback" |
undefined || "fallback" | "fallback" | "fallback" |
// Real-world: user sets volume to 0 (valid!)
function setVolume(level) {
// BUG with ||
const vol1 = level || 50; // 0 → 50 (wrong! user wanted 0)
// CORRECT with ??
const vol2 = level ?? 50; // 0 → 0 (correct!)
return vol2;
}
console.log(setVolume(0)); // 0 (correct)
console.log(setVolume(null)); // 50 (correct)
console.log(setVolume()); // 50 (correct — undefined)
Cannot mix ?? with && or || without parentheses
// SyntaxError: cannot mix ?? with && or || without parentheses
// null || undefined ?? "default" // ERROR
// Must use parentheses
(null || undefined) ?? "default"; // "default"
null || (undefined ?? "default"); // "default"
5. Operator precedence: NOT > AND > OR
// NOT has highest precedence
console.log(!true || false); // false || false → false
console.log(!(true || false)); // !true → false
// AND binds tighter than OR
console.log(true || false && false);
// Evaluates as: true || (false && false) → true || false → true
console.log((true || false) && false);
// (true) && false → false
Full precedence order for logical operators
| Precedence (high → low) | Operator |
|---|---|
| 1 | ! (NOT) |
| 2 | && (AND) |
| 3 | ` |
| 4 | ?? (Nullish coalescing) |
// Complex example
const result = !false && true || false;
// Step 1: !false → true
// Step 2: true && true → true
// Step 3: true || false → true
console.log(result); // true
Best practice: Use parentheses whenever you combine more than two logical operators. Do not rely on precedence for readability.
6. Using logical operators for conditional assignment
&& for conditional execution
const isAdmin = true;
isAdmin && console.log("Admin panel loaded"); // logs it
const isGuest = false;
isGuest && console.log("Guest mode"); // does NOT log
|| for fallback values
const config = {
timeout: userTimeout || 3000,
retries: userRetries || 3,
theme: userTheme || "light",
};
?? for safer fallbacks
const settings = {
volume: userVolume ?? 50, // 0 is valid
nickname: userNickname ?? "Player", // "" is valid
darkMode: userDarkMode ?? false, // false is valid
};
7. Guard patterns and default values in depth
Nested guard (before optional chaining)
// Old pattern
const zipCode = user && user.address && user.address.zip;
// Modern equivalent
const zipCode2 = user?.address?.zip;
// With default
const zipCode3 = user?.address?.zip ?? "N/A";
Function parameter defaults vs ||/??
// ES6 default parameters only kick in for undefined (not null)
function createUser(name = "Anonymous") {
return { name };
}
console.log(createUser()); // { name: "Anonymous" }
console.log(createUser(undefined)); // { name: "Anonymous" }
console.log(createUser(null)); // { name: null } (default NOT used!)
console.log(createUser("")); // { name: "" }
// Use ?? inside the function for null handling too
function createUser2(name) {
return { name: name ?? "Anonymous" };
}
console.log(createUser2(null)); // { name: "Anonymous" }
Chaining ?? with optional chaining
const config = {
api: {
// timeout might be 0 (valid)
}
};
const timeout = config?.api?.timeout ?? 5000;
console.log(timeout); // 5000 (undefined → default)
8. Real-world patterns in React/JS codebases
Conditional rendering
// React JSX pattern
// {items.length > 0 && <ItemList items={items} />}
// {error && <ErrorMessage error={error} />}
// {isLoading ? <Spinner /> : <Content />}
Feature flags
const features = {
darkMode: true,
betaFeature: false,
};
features.darkMode && enableDarkMode();
features.betaFeature && showBetaBanner();
Configuration merging
function createConfig(userOptions) {
return {
host: userOptions?.host ?? "localhost",
port: userOptions?.port ?? 3000,
debug: userOptions?.debug ?? false,
timeout: userOptions?.timeout ?? 5000,
};
}
console.log(createConfig({ port: 8080, debug: true }));
// { host: "localhost", port: 8080, debug: true, timeout: 5000 }
console.log(createConfig({ port: 0 }));
// { host: "localhost", port: 0, debug: false, timeout: 5000 }
// port: 0 is preserved! (|| would have replaced it with 3000)
Short-circuit for lazy evaluation
// Expensive function only called if needed
const cachedResult = cache.get(key) ?? expensiveComputation(key);
Key takeaways
&&returns the first falsy value or the last value;||returns the first truthy value or the last value.- Both
&&and||use short-circuit evaluation — the right side may never execute. !converts to boolean and inverts;!!is a common boolean-casting idiom.??only falls back fornull/undefined— it preserves0,"", andfalseas valid values.- Precedence:
!>&&>||>??— use parentheses for clarity. ||for defaults has a flaw: it replaces all falsy values. Prefer??when0,"", orfalseare valid.- You cannot mix
??with&&/||without parentheses — this is a syntax error by design.
Explain-It Challenge
Explain without notes:
- What does
0 || "fallback"return? What about0 ?? "fallback"? Why are they different? - In
a && b, under what conditions isbnever evaluated? - What is the double negation
!!pattern, and when would you use it? - Write a one-liner that safely accesses
user.address.citywith a fallback of"Unknown". - Why can you not write
a || b ?? cwithout parentheses in JavaScript?
Navigation: ← 1.18.b — Comparison Operators · 1.18.d — Assignment Operators →