Logical operators
Learn JavaScript's &&, ||, and ! operators: truthy and falsy decisions, short-circuit evaluation, operand return values, guard patterns, and safe defaults.
- 01Read logical expressionsPredict how &&, ||, and ! combine truthy and falsy values.
- 02Trace short-circuitingKnow when JavaScript skips the right side of an expression.
- 03Use practical patternsWrite guard calls and defaults without being surprised by 0, '', null, or undefined.
Three operators, one idea
Logical operators help JavaScript answer questions like “do we have both requirements?”, “do we have at least one usable value?”, and “is this not true?” The three operators are && (and), || (or), and ! (not).
They build on truthy and falsy values from Booleans & truthiness. A value like "Ada", 10, or [] is truthy. The eight falsy values are false, 0, -0, 0n, "", null, undefined, and NaN.
Logical operators test truthiness, may skip the right side, and—except for !—return one of the original operands.
Imagine a door that needs a badge and a PIN. If the badge is missing, the guard does not bother asking for the PIN. The first missing requirement already decides the answer.
- In real life: A guard asks for a badge
- In JavaScript: The left operand of
&& - In real life: Then the guard asks for a PIN
- In JavaScript: The right operand of
&& - In real life: No badge? Stop immediately
- In JavaScript: A falsy left value short-circuits
- In real life: Badge and PIN? Enter
- In JavaScript: Both operands must be truthy
Where the analogy stops: A real guard might ask questions in any order. JavaScript always evaluates left to right for these operators.
This lesson goes deeper than a boolean truth table. You will step through skipped function calls, experiment with the exact returned value, use guards and defaults, and see why the next lesson’s ?? fixes the classic 0 || 10 bug.
&&, ||, and !
TABLESWith plain booleans, the operators are easy to read. a && b means both must be truthy. a || b means at least one must be truthy. !a flips truthiness and always gives the boolean true or false.
| Expression | When it is true | Examples |
|---|---|---|
a && b | Only when a and b are both true | true && true → true; true && false → false |
a || b | When a or b (or both) are true | false || true → true; false || false → false |
!a | When a is falsy | !false → true; !true → false |
But JavaScript usually meets values that are not already booleans. It asks “truthy or falsy?” for the decision, then keeps the actual value around. That is why "cat" && 10 returns 10, not true.
| Code | Truthiness decision | Returned value |
|---|---|---|
Boolean("cat") | The string is truthy | true because Boolean() converts |
"cat" && 10 | Left is truthy, so use the right side | 10, the actual right operand |
0 || "Guest" | Left is falsy, so use the right side | "Guest", the actual right operand |
!"cat" | The string is truthy, then flipped | false, because ! always returns a boolean |
Short-circuit evaluation
STEP THROUGHShort-circuiting means JavaScript stops as soon as the result is known. The right side might not run at all. This matters when the right side is a function call, a property read, or anything with a side effect.
The || operator is like trying keys on a keyring. The first key that opens the lock wins. You do not keep trying keys after the door is open.
- In real life: Try the front-door key
- In JavaScript: Evaluate the left operand
- In real life: It opens? Stop trying keys
- In JavaScript: Truthy left value short-circuits
|| - In real life: It fails? Try the next key
- In JavaScript: Falsy left value makes JavaScript evaluate the right operand
Where the analogy stops: A person might try keys randomly. JavaScript tries operands left to right and stops by fixed rules.
Predict which helper runs. Then step through the real recording: the skipped helper will have no frame at all.
script
function checkBadge() { console.log("badge checked"); return "";}function checkPin() { console.log("PIN checked"); return "pin-ok";const result = checkBadge() && checkPin();console.log(result);Watch the skipped function: no log, no return value, no frame. That is real JavaScript control flow, not just a display trick. This is why isLoggedIn && showDashboard() calls the function only when the guard is truthy.
"badge" && checkPin()0 && saveScore()"Ada" || "Guest""" || "Guest"user && user.namenull && user.name
Sort each expression by whether JavaScript evaluates the right operand.
They return operands, not booleans
INTERACTIVEThis is the rule that surprises beginners and helps professionals write compact code: && and || return the value that decided the expression. They do not hand you a yes/no card unless one operand was already a boolean.
a && breturnsaifais falsy; otherwise it returnsb.a || breturnsaifais truthy; otherwise it returnsb.!aalways returns a boolean.!!ais a common way to convert truthiness to exactlytrueorfalse.
const left = "cat";const right = 10;console.log(left && right);console.log(Boolean(left && right));console.log(!left);console.log(!!left);- Returned operand
10- Boolean(result)
true- !left
false- !!left
true
The expression returns 10. Boolean(result) is true. !left is false, and !!left is true.
An empty array [] looks empty, but it is still an object, and objects are truthy. Try [] || "fallback" in the playground: the array wins.
Precedence and De Morgan
STEP THROUGHPrecedence decides which operator groups first when there are no parentheses. For logical operators, remember this order: ! binds tightest, then &&, then ||. So true || false && false is true || (false && false), which is true.
Step through the four lines to see precedence and one De Morgan rewrite.
script
console.log(true || false && false);console.log((true || false) && false);console.log(!(true && false) === (!true || !false));Parentheses are often better than relying on memory. They make your intent visible to the next person reading the code, including future you.
console.log(!false && false);console.log(true || false && false);console.log((true || false) && false);console.log(!(true && false) === (!true || !false));De Morgan’s laws help you move ! inward: !(a && b) has the same boolean meaning as !a || !b. In words, “not both requirements passed” means “the badge failed or the PIN failed.”
Guard and default patterns
INTERACTIVEShort-circuiting creates two beginner-friendly patterns. A guard only continues when the left side is safe: user && user.name avoids reading name from null. A default chooses a backup: name || "Guest" uses Guest when name is falsy.
const user = { name: "Ada" };const missingUser = null;console.log(user && user.name);console.log(missingUser && missingUser.name);console.log("" || "Guest");console.log(0 || 10);- user && user.name
null- name || "Guest"
"Guest"- count || 10
10- count ?? 10
0
user && user.name gives null. name || "Guest" gives "Guest". count || 10 gives 10, while the next lesson's count ?? 10 would keep 0.
The default pattern is useful, but it treats every falsy value as missing. That is sometimes wrong: 0 || 10 returns 10, even when 0 is a real count. The next lesson, Nullish coalescing & optional chaining, teaches ?? for “only default when the value is null or undefined,” and ?. for replacing long guard chains.
const user = { name: "Ada" };const missingUser = null;console.log(user && user.name);console.log(missingUser && missingUser.name);console.log("" || "Guest");console.log(0 || 10);Where you’ll use this
Logical operators appear anywhere code needs a small decision inside an expression. You will see them in setup checks, display defaults, conditional calls, and validation.
isLoggedIn && showDashboard()— call a function only when a condition is truthy.const label = nickname || "Guest";— display a fallback when the primary value is empty.const city = user && user.address && user.address.city;— older guard chains; next lesson replaces this withuser?.address?.city.if (isAdmin && hasToken) { ... }— when a statement reads clearer than a clever expression.
Short-circuit calls are handy for tiny actions. If you need several lines, an if statement is clearer and easier to debug. Control flow gets its own module soon.
Common misconceptions
- “Logical operators always return booleans.” Only
!does.&&and||return operands. - “The right side always runs.” Short-circuiting may skip it completely.
- “Falsy means useless.”
0and""can be valid data. Be careful with||defaults. - “
!!xis the same asx.” It has the same truthiness, but the value becomes exactlytrueorfalse. - “Precedence is obvious.” It is easy to misread mixed
&&and||. Parentheses are cheap.
Practice exercises
5 EXERCISESPredict all three console outputs before you run the code.
console.log("cat" && 10);
console.log(0 || "fallback");
console.log(!"");console.log("cat" && 10);
console.log(0 || "fallback");
console.log(!"");The string cat is truthy, so the first line prints 10. 0 is falsy, so the second line prints fallback. The empty string is falsy, so !"" is true.
Find the surprising output, then rewrite the default in the solution.
const count = 0;
console.log(count || 10);const count = 0;
console.log(count ?? 10);The buggy count || 10 prints 10 because 0 is falsy. Use count ?? 10 when only null or undefined should mean missing.
Use && so the property read only happens when user exists.
const user = null;
console.log(user && user.name);const user = null;
console.log(user && user.name);user && user.name returns null when user is null. The right side is skipped, so there is no property-read error.
Trace the && chain and decide whether the function call runs.
const isLoggedIn = true;
const hasPlan = true;
function showDashboard() { console.log("dashboard"); }
isLoggedIn && hasPlan && showDashboard();const isLoggedIn = true;
const hasPlan = true;
function showDashboard() { console.log("dashboard"); }
isLoggedIn && hasPlan && showDashboard();Because both conditions are true, JavaScript reaches showDashboard() and prints dashboard. For longer work, a normal if would be clearer.
Rewrite the condition without changing its boolean meaning.
const hasBadge = true;
const hasPin = false;
console.log(!(hasBadge && hasPin));
console.log(!hasBadge || !hasPin);!hasBadge || !hasPin!(hasBadge && hasPin) means at least one requirement is missing. De Morgan rewrites that as !hasBadge || !hasPin; both lines print true for badge true and PIN false.
Check your understanding
7 QUESTIONSQuestion 1 of 7What does cat && dog print?
Read the code, then predictconsole.log("cat" && "dog");Choose an answer to see the explanation.
Question 2 of 7Which expression skips the function call?
Choose an answer to see the explanation.
Question 3 of 7What does an empty-name default print?
Read the code, then predictconsole.log("" || "Guest");Choose an answer to see the explanation.
Question 4 of 7What does !0 print?
Read the code, then predictconsole.log(!0);Choose an answer to see the explanation.
Question 5 of 7How is
true || false && falsegrouped?Choose an answer to see the explanation.
Question 6 of 7What is the safer next-lesson fix for
count || 10when 0 should be kept?Choose an answer to see the explanation.
Question 7 of 7Which rewrite matches
!(hasBadge && hasPin)?Choose an answer to see the explanation.
Key takeaways
&&needs both sides to be truthy; it stops on the first falsy operand.||needs one truthy side; it stops on the first truthy operand.!flips truthiness and always returns a boolean.&&and||return operands, not converted booleans.- Use guards and defaults carefully;
0and""are falsy but often meaningful.
Logical operators combine truthy/falsy decisions with left-to-right short-circuiting, returning the operand that decides the expression.
Up next: Nullish coalescing & optional chaining.