JAVASCRIPT / OPERATORS
Logical operators and short-circuit results
Predict the value and type that &&, || and ! produce in JavaScript, and know exactly which operand never gets evaluated.
What you will learn
- Read a || b as 'the first truthy operand, otherwise the last one'
- Predict which operand && or || skips, and use it to guard expensive calls
- Name the eight falsy values so you see when || replaces a valid 0 or ''
- Reach for ! or !! when code needs a real boolean, since && and || return operands
Understanding Logical operators and short-circuit results
In JavaScript && and || are selection operators, not boolean operators. Each tests the truthiness of its left operand and then returns one of the two operands unchanged, never a converted true or false. Read a || b as 'the first truthy operand, or the last one if none are truthy', and a && b as 'the first falsy operand, or the last one if none are falsy'. That is why 0 || 'sold out' is a string while 0 && 'in stock' is the number 0.
Short-circuiting follows directly from that rule: once the left operand decides the answer, the right operand is never evaluated at all. This is specified behavior rather than an optimization, so you can rely on it — cache || expensiveLoad() calls nothing when the cache hits, and list && list.length never touches .length when list is null. The flip side is that a log statement, an increment, or a network call parked on the right of a && can silently never happen.
Everything hinges on the falsy set: false, 0, -0, 0n, '', null, undefined, NaN. Every other value is truthy, including '0', 'false', [] and {}. That is why || makes a poor default operator for numbers and strings, since a deliberate 0 or '' triggers the fallback. Of the three operators only ! always produces a boolean, which is why !!value is the idiomatic truthiness cast; also note that && binds tighter than ||, so a || b && c means a || (b && c).
Because && and || hand back operands, their results carry the original type, and that type leaks into whatever consumes them: JSON payloads, string concatenation, and template output. Checking the result with typeof is the fastest way to catch a logical expression that returned 0 or '' where the surrounding code expected true or false.
const stock = 0;
const label = 'in stock';
console.log(stock || 'sold out');
console.log(stock && label);
console.log(typeof (stock && label));
console.log(!stock);
let checks = 0;
function slowCheck() {
checks++;
return true;
}
const a = stock > 0 && slowCheck();
console.log(a, 'checks:', checks);
const b = stock === 0 || slowCheck();
console.log(b, 'checks:', checks);
const c = stock === 0 && slowCheck();
console.log(c, 'checks:', checks);&& and || return one of their operands instead of a boolean, and they stop evaluating as soon as the left operand determines the result.
Worked examples
The zero problem with || defaults
Shows how a falsy but meaningful argument is swallowed by an || fallback.
function volume(level) {
return level || 50;
}
console.log(volume(80));
console.log(volume(undefined));
console.log(volume(0));
const inputs = [0, '', null, undefined, NaN, false, '0', []];
console.log(inputs.map(v => (v ? 'truthy' : 'falsy')).join(' '));Example explained
Line 1level || 50 returns level only when level is truthy, so volume(80) gives 80 back unchanged.
Line 2volume(0) prints 50 because 0 is falsy, so a muted volume becomes half volume.
Line 3The map line walks the falsy set: '' and NaN take the falsy branch, while '0' and [] do not.
Chains and the calls that never run
Demonstrates which operand a long && or || chain returns and that skipped calls leave no trace.
const config = { retries: 0 };
function announce(msg) {
console.log('announce:', msg);
return msg.length;
}
console.log(null || 0 || '' || 'first truthy' || announce('never runs'));
console.log('a' && 1 && true && 'last value');
const guard = config.retries && announce('retrying');
console.log('guard:', guard);Example explained
Line 1The || chain stops at 'first truthy', so announce is never called and no announce: line appears.
Line 2'a' && 1 && true && 'last value' contains no falsy operand, so && runs to the end and returns the final operand.
Line 3config.retries is 0, so guard is 0 rather than false: && returned the very operand that made the decision.
Precedence and forcing a boolean
Shows that && groups before ||, and that only ! guarantees a boolean result.
const isAdmin = true;
const isOwner = false;
const isBanned = true;
console.log(isAdmin || isOwner && !isBanned);
console.log((isAdmin || isOwner) && !isBanned);
console.log(!isBanned, typeof !isBanned);
console.log(!!'false', !!'');Example explained
Line 1Line 1 parses as isAdmin || (isOwner && !isBanned) because && binds tighter, so a banned admin still passes.
Line 2The parentheses on line 2 apply the ban check to both roles, flipping the result to false.
Line 3!isBanned is a genuine boolean, which is why typeof reports boolean instead of the operand's own type.
Line 4!!'false' is true because any non-empty string is truthy; only '' is the falsy string.
Important notes
&& and || branch on truthiness, so 0, '' and NaN take the falsy path; falling back only for null and undefined is a different operator.
Since the right operand may never run, side effects hidden there can go unobserved for a long time; keep anything that must happen out of logical expressions.
Common mistakes
Using count || 10 as a default: a real 0 is falsy, so the fallback fires and computed totals come out wrong.
Expecting arr.length && 'has items' to be a boolean; an empty array makes it 0, which then gets serialized or rendered as 0.
Writing if (day === 6 || 7) instead of day === 6 || day === 7; the bare 7 is truthy, so the branch runs for every day.
Try it yourself
Change, predict, then run
In a browser console define const label = n => n && n + ' items', call it with 0, 1 and 5, and log typeof for each result. Then rewrite label so every call returns a string while the 0 case still reads differently from the others.
Open the JavaScript workspaceCheck your understanding
A lookup helper returns cache[key] || fetchValue(key), where fetchValue is expensive and cached values are sometimes the number 0. What actually goes wrong?
- fetchValue never runs, because || always returns its left operand
- The expression yields true instead of the cached value, so callers receive a boolean
- fetchValue runs whenever the cached value is 0, because 0 is falsy
- fetchValue runs on every call, because || evaluates both operands before choosing
Show answer
|| decides by truthiness, and 0 is falsy, so a perfectly good cached 0 is discarded and the expensive call runs every time that key is requested. The last option is wrong because || stops as soon as the left operand is truthy, so caching still works for every non-zero value, which is exactly what makes this bug easy to miss.