JAVASCRIPT / CONDITIONALS
Boolean logic with AND, OR, and NOT
Combine conditions with &&, ||, and !, predict which operand each returns, exploit short-circuiting, and negate compound tests with De Morgan's laws.
What you will learn
- Read a && b as: hand back a when a is falsy, otherwise hand back b
- Use !! or Boolean() when you need a real true/false instead of an operand
- Order operands so short-circuiting skips the unsafe or expensive side
- Negate compound conditions with De Morgan: flip operands and swap && with ||
Understanding Boolean logic with AND, OR, and NOT
&& and || are usually called boolean operators, but neither one produces a boolean. Each evaluates its left operand, tests only that operand's truthiness, and then returns one of the two operands untouched: a && b gives back a when a is falsy and b otherwise, while a || b gives back a when a is truthy and b otherwise. That is why 0 || 3 is 3 and 1 && 2 is 2 — these operators choose a value, they do not compute a truth value. ! is the exception: it always coerces and always yields true or false, which is why !!x is the shortest way to force a real boolean.
The choice is made before the second operand is evaluated at all, and that short-circuiting is part of the semantics, not an optimisation detail. If the left operand of && is falsy the right side never runs, so a function call, assignment, or property access sitting there simply does not happen; || skips its right side when the left is truthy. This is what makes user && user.name safe on a null user, and it means the order of the operands is part of what the expression means rather than a matter of taste.
When operators mix, grouping comes from precedence: ! binds tightest, then &&, then ||, so a || b && c parses as a || (b && c) and never as (a || b) && c. Precedence decides parsing, not evaluation order — in true || (x && y) the group is parsed first and still never evaluated. For negations, apply De Morgan's laws instead of intuition: !(a && b) is !a || !b, and !(a || b) is !a && !b, flipping each operand and swapping the operator. Forgetting the swap is how a test meant to catch "either one is missing" quietly becomes "both are missing".
const title = "";
const fallback = "untitled";
// && returns the first falsy operand, otherwise the last one
console.log(JSON.stringify(title && title.toUpperCase()));
// || returns the first truthy operand, otherwise the last one
console.log(JSON.stringify(title || fallback));
// ! is the only one of the three that always produces a boolean
console.log(!title, typeof (title || fallback));
function probe(tag) {
console.log("probe ran for " + tag);
return true;
}
const skipped = false && probe("A");
const evaluated = true && probe("B");
console.log(skipped, evaluated);&& and || select and return one of their operands based on truthiness, evaluating the right side only when the left has not already decided the answer.
Worked examples
Precedence decides the grouping
Shows that && binds tighter than ||, so leaving out parentheses changes which comparison is grouped with which.
const cached = true;
const online = false;
const stale = true;
console.log(cached || online && !stale);
console.log(cached || (online && !stale));
console.log((cached || online) && !stale);Example explained
Line 1In line 1 the parser attaches ! to stale alone and binds online && !stale into a single operand of ||.
Line 2Lines 1 and 2 print the same value, which is the proof that the implicit grouping is a || (b && c).
Line 3Line 3 forces the || to run first, so the && now sees !stale, which is false, and the result flips.
Line 4Because cached is true, the right-hand group in lines 1 and 2 is never evaluated: precedence set the shape, short-circuiting decided what ran.
De Morgan's laws in a truth table
Compares !(a && b) against both !a || !b and the common wrong rewrite !a && !b over every combination.
const rows = [[true, true], [true, false], [false, true], [false, false]];
console.log("a b !(a&&b) !a||!b !a&&!b");
for (const [a, b] of rows) {
console.log(
String(a).padEnd(7) +
String(b).padEnd(7) +
String(!(a && b)).padEnd(9) +
String(!a || !b).padEnd(8) +
String(!a && !b)
);
}Example explained
Line 1Columns three and four match on all four rows, which is exactly the law !(a && b) === !a || !b.
Line 2Column five differs on the two mixed rows, because !a && !b is the negation of a || b, not of a && b.
Line 3The padEnd calls only align the columns; String() prints each boolean unchanged.
The operators return values, not booleans
Demonstrates that && and || hand back an operand, including the case where a falsy 0 loses to a fallback.
const config = { retries: 0, host: " api.dev " };
console.log(1 || 2, 1 && 2, "" || "fallback");
console.log(config.retries || 3);
console.log(!!config.retries, !!config.host);
console.log(config.host && config.host.trim());
console.log(typeof (config.retries || 3), typeof !!config.retries);Example explained
Line 11 || 2 is 1 and 1 && 2 is 2: each returns an operand, and neither result is true.
Line 2config.retries is 0, which is falsy, so || replaces a real setting with the fallback 3.
Line 3config.host is a truthy string, so && evaluates the right side and the expression is the trimmed string.
Line 4typeof makes the split visible: the || expression is a number here, while !! is always boolean.
Important notes
Comparisons do not chain: 1 < n < 10 first computes 1 < n as a boolean, then compares that (coerced to 1 or 0) against 10, so it is true for almost every n. Write n > 1 && n < 10.
?? looks like || but tests only for null and undefined, so 0 ?? 3 is 0 while 0 || 3 is 3; use ?? when zero or an empty string are valid values.
Common mistakes
Writing status === "open" || "closed" as a shortcut: the second operand is the truthy string "closed", so the expression is always truthy and the test passes for every status.
Negating !(isAdmin && isActive) into !isAdmin && !isActive: the rewrite is true only when both flags are false, so cases where exactly one is missing slip through.
Using port || 8080 for a default: a deliberate 0 or "" is falsy, so the fallback silently overwrites a legitimate value.
Try it yourself
Change, predict, then run
In a browser console, define outside = n => !(n >= 1 && n <= 10) and its De Morgan twin outside2 = n => n < 1 || n > 10, then log both for n = 0, 1, 10, and 11 and confirm every pair agrees.
Open the JavaScript workspaceCheck your understanding
Given that && and || do not always produce true or false, what does 0 || "" || null evaluate to, and why?
- false, because every operand is falsy so the whole expression collapses to false
- 0, because || returns the first operand it inspects
- null, because || returns the last operand when it never finds a truthy one
- undefined, because there is no truthy operand available to return
Show answer
|| walks left to right returning the first truthy operand; with nothing truthy it runs out of choices and returns the final operand as it is, so the value is null. "false" is tempting because the expression is falsy overall, but only ! coerces — && and || hand back operands, which is also why 1 || 2 is 1 rather than true.