The ternary operator and the logical operators && and || are two sides of the same idea: write an expression that produces a value instead of a statement that assigns one. They are also the source of a family of bugs, because both return one of their operands rather than a boolean — a detail that surprises almost everyone the first time they hit it.
What is the ternary operator?
It is an expression with three parts: a condition, a value to use when it is true, and a value to use when it is false. The question mark and the colon are the only punctuation it needs.
const age = 20;
const status = age >= 18 ? "adult" : "minor";
console.log(status);
const greeting = age >= 18
? `Welcome, ${age}-year-old.`
: "Sorry, you must be 18 or older.";
console.log(greeting);
The first line prints adult, the second prints Welcome, 20-year-old.
Because a ternary is an expression, it can appear anywhere a value can: inside a template literal, as a function argument, in an object literal, or assigned to a variable. That flexibility is its main advantage over if/else, which is a statement and can only stand on its own.
When is a ternary better than if/else?
When the whole decision is one value. If the branches are a single expression each, the ternary keeps the value and the condition on adjacent lines, and the reader sees the whole choice at once.
const score = 85;
// Nested ternary: compact, but the branches stack up.
const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F";
console.log(grade); // "B"
// Guard style: one condition per line, no nesting.
function gradeReadable(score) {
if (score >= 90) return "A";
if (score >= 80) return "B";
if (score >= 70) return "C";
return "F";
}
console.log(gradeReadable(85)); // "B"
Both produce B. The first fits on one line but you have to hold three levels in your head to read it, and adding a fourth grade makes it worse. The second takes four lines and reads top to bottom without effort.
A useful rule: one ternary is fine, nested ternaries are a smell. If you find yourself reaching for a second ? :, an if with early returns is clearer.
What does short-circuiting mean?
&& and || do not evaluate both sides. They evaluate the left side first and decide whether the right side needs to run at all. That is short-circuiting, and it is what makes && useful as a guard.
const user = { name: "Ada", settings: null };
// && guards: the right side only runs if the left side is truthy.
console.log(user.settings && user.settings.theme); // null
// || picks the first truthy value.
console.log(user.nickname || user.name); // "Ada"
The first line is the pattern you will write most often: check that something exists before you reach into it. If user.settings were null, the && would stop there and hand back the null itself, rather than throwing a TypeError on user.settings.theme.
Short-circuiting also means side effects do not happen. Watch which of these two calls runs:
function announce(message) {
console.log("called with:", message);
return true;
}
const enabled = true;
enabled && announce("&& ran the call"); // the call happens
enabled || announce("|| ran the call"); // the call is skipped
console.log("done");
The output is one line from announce, then true, then true, then done. With &&, the left side was truthy, so the right side ran. With ||, the left side was truthy, so the right side was skipped entirely.
What do && and || actually return?
Not booleans. They return one of their operands — the first falsy value for &&, the first truthy value for ||. This is why someString && someString.trim() is a safe way to trim, and why value || "fallback" is a safe way to supply a default.
console.log("Ada" && "logged in"); // "logged in"
console.log("" && "logged in"); // ""
console.log(0 || "fallback"); // "fallback"
console.log("first" || "second"); // "first"
console.log(null || undefined); // undefined
The second line is the one to internalise. "" && "logged in" returns the empty string, not false, because && hands back the first operand when it is falsy. If your code assigns that to a variable and then checks if (result), the empty string is falsy, so it behaves like false — but the value itself is not a boolean, and that difference can surface later.
Why is ?? better than || for defaults?
Because || treats every falsy value as missing, and not every falsy value should be replaced. A port of 0, a discount of 0 and an empty string are all meaningful.
const config = { retries: 0, timeout: 5000 };
console.log(config.retries || 3); // 3 ← the 0 was replaced
console.log(config.retries ?? 3); // 0 ← the 0 survived
console.log(config.timeout ?? 1000); // 5000
The nullish coalescing operator ?? only falls back when the left side is null or undefined. Everything else passes through untouched.
One important restriction: you cannot mix ?? with && or || without parentheses. The engine throws a SyntaxError rather than guess which precedence you meant. That is a deliberate guard, and the fix is to add brackets so the intent is explicit — (a || b) ?? c — even when the parentheses look redundant.
What are the common ternary and short-circuit bugs?
Three patterns worth recognising on sight.
The truthy-value bug. Using && for a value that can legitimately be false or 0. isActive && renderToggle() puts false in your variable instead of the return value of the function. Use a ternary when you need a definite value.
The fallback bug. Using || where ?? is meant. pageCount || 1 turns a deliberate zero into a one.
The assignment-in-condition bug. Writing if (x = getValue()) instead of if (x === getValue()). The single equals assigns, and the condition then tests the assigned value, which is truthy for almost everything. It passes review because it looks like a typo, but it is valid code that changes behaviour.
How to use these operators well
- A ternary is an expression, so it can be assigned, interpolated or passed as an argument. That is when to prefer it over
if/else. - One ternary is readable. Nested ternaries are not — switch to early returns.
&&and||short-circuit, which makes them good guards and good defaults.- Both return an operand, not a boolean.
&&returns the first falsy value,||the first truthy one. - Use
??when only null andundefinedshould trigger the fallback, and||when any falsy value should. - Never mix
??with&&or||without brackets — the engine will not guess.
Try it yourself: write a function that takes a cart total and a discount and returns the amount due, where a discount of zero must stay zero. Implement it once with || and once with ??, and test both with a discount of 0. Then rewrite the || version as a ternary so the intent is unambiguous, and note which of the three versions you would be happiest to read in six months.