Logical Operators
Logical operators let you combine conditions and make decisions: “this and that”, “this or that”, “not this”. In JavaScript they go further than simple true/false logic — && and || short-circuit and return one of their actual operands rather than a boolean, which powers a whole family of compact idioms for defaults and guards. The newer ?? (nullish coalescing) operator fills a long-standing gap left by ||. Understanding how truthiness drives all of this is essential to writing concise, bug-free code.
The three core operators
JavaScript has three logical operators. && (AND) and || (OR) are binary; ! (NOT) is unary.
| Operator | Name | Example | Result |
|---|---|---|---|
&& | Logical AND | true && false | false |
|| | Logical OR | true || false | true |
! | Logical NOT | !true | false |
The ! operator always returns a real boolean: it coerces its operand to a boolean and flips it. Applying it twice (!!value) is a common trick to coerce any value to its boolean equivalent.
console.log(!0); // true (0 is falsy)
console.log(!"hello"); // false ("hello" is truthy)
console.log(!!"hello"); // true
console.log(!![]); // true (empty array is truthy)
Output:
true
false
true
true
The falsy values in JavaScript are exactly:
false,0,-0,0n,"",null,undefined, andNaN. Everything else is truthy — including"0","false",[], and{}.
Short-circuit evaluation
This is the part that trips up developers coming from other languages: && and || do not return true or false. They evaluate operands left to right and return one of the original operands, stopping as soon as the result is determined.
a && bevaluatesa. Ifais falsy, it returnsaimmediately (no need to checkb). Otherwise it returnsb.a || bevaluatesa. Ifais truthy, it returnsaimmediately. Otherwise it returnsb.
console.log(0 && "anything"); // 0 (stops at falsy left operand)
console.log("hi" && 42); // 42 (left is truthy, returns right)
console.log("" || "fallback"); // fallback
console.log("name" || "guest"); // name (stops at truthy left operand)
Output:
0
42
fallback
name
Because the right side is skipped entirely when the result is already known, you can use short-circuiting to guard expensive or unsafe operations:
const user = getUser();
// Only call .toUpperCase() if user and user.name exist
const display = user && user.name && user.name.toUpperCase();
// Run a function only when a condition holds
isLoggedIn && trackEvent("page_view");
The first line returns undefined (or whatever falsy value stopped it) instead of throwing, which is why short-circuiting was the classic way to avoid “Cannot read property of undefined” errors before optional chaining (?.) arrived.
Defaults with ||
The OR operator is frequently used to supply a fallback value: value || defaultValue returns value unless it is falsy.
function greet(name) {
const who = name || "friend";
return `Hello, ${who}!`;
}
console.log(greet("Ada")); // Hello, Ada!
console.log(greet("")); // Hello, friend!
console.log(greet()); // Hello, friend!
Output:
Hello, Ada!
Hello, friend!
Hello, friend!
This works well — until a legitimate value happens to be falsy. If 0, "", or false are valid inputs, || will wrongly discard them. That is exactly the problem ?? solves.
Nullish coalescing: ?? vs ||
The nullish coalescing operator (ES2020) returns its right-hand side only when the left-hand side is null or undefined — not for every falsy value. It treats 0, "", and false as real, usable values.
const count = 0;
console.log(count || 10); // 10 (|| rejects the falsy 0)
console.log(count ?? 10); // 0 (?? keeps 0, only nullish triggers fallback)
const config = { volume: 0, label: "" };
const volume = config.volume ?? 75; // 0 (respected)
const label = config.label ?? "Untitled"; // "" (respected)
Output:
10
0
| Left value | left || right | left ?? right |
|---|---|---|
null | right | right |
undefined | right | right |
0 | right | 0 |
"" | right | "" |
false | right | false |
"value" | "value" | "value" |
Gotcha: You cannot mix
??with&&or||without parentheses —a ?? b || cis a syntax error. Write(a ?? b) || cto make precedence explicit. This restriction exists deliberately to prevent ambiguous code.
Practical idioms
Combining these operators yields several patterns you’ll see constantly:
// Guarded property access (pre-optional-chaining style)
const city = (user && user.address && user.address.city) || "Unknown";
// Default options object merged with caller overrides
function connect(options = {}) {
const host = options.host ?? "localhost";
const port = options.port ?? 5432;
return `${host}:${port}`;
}
console.log(connect()); // localhost:5432
console.log(connect({ port: 0 })); // localhost:0 (?? keeps 0)
Output:
localhost:5432
localhost:0
For mutating assignments, the logical assignment operators (&&=, ||=, ??=) combine the operator with = and also short-circuit:
const settings = {};
settings.theme ??= "dark"; // assigns only if theme is null/undefined
settings.theme ??= "light"; // no-op, theme already set
console.log(settings.theme); // dark
Output:
dark
Best Practices
- Reach for
??when0,"", orfalseare valid values; reserve||for true “any falsy means missing” cases. - Wrap
??in parentheses whenever it sits next to&&or||— the engine requires it and it documents intent. - Use
!!valueto explicitly coerce to a boolean rather than relying on implicit truthiness in return values. - Prefer optional chaining (
?.) over longa && a.b && a.b.cguard chains for cleaner null-safe access. - Keep short-circuit side effects (
cond && doThing()) for quick guards, but use a realifwhen the logic grows beyond one expression — readability wins. - Remember the falsy list; most truthiness bugs come from forgetting that
[]and"0"are truthy.