JavaScript Operators
Master JavaScript operators — arithmetic, comparison, logical, nullish coalescing, optional chaining, spread, and ternary — with real-world examples.
Arithmetic Operators
Arithmetic operators perform math on numeric values and are the foundation for any calculation in your program. Standard math operations work as expected, with a few JavaScript-specific behaviors worth knowing — particularly around +, which doubles as string concatenation, and the way operator precedence interacts with mixed types.
console.log(10 + 3); // 13
console.log(10 - 3); // 7
console.log(10 * 3); // 30
console.log(10 / 3); // 3.3333...
console.log(10 % 3); // 1 (remainder / modulo)
console.log(10 ** 3); // 1000 (exponentiation, ES2016)
// Increment / decrement
let x = 5;
x++; // post-increment: use x, then add 1
++x; // pre-increment: add 1, then use x
x--; // post-decrement
--x; // pre-decrement
// + with strings: concatenation, not addition
"5" + 3 // "53" ← 3 is coerced to "3"
5 + 3 + "px" // "8px" ← left to right: 5+3=8, then "8"+"px"
"px" + 5 + 3 // "px53" ← "px"+"5"="px5", then "px5"+"3"
Assignment Operators
Assignment operators combine a math operation with assignment in a single step, reducing repetition. They’re the idiomatic way to update a variable based on its current value. ES2021 also added logical assignment operators that conditionally assign only when certain conditions are met.
let n = 10;
n += 5; // n = n + 5 → 15
n -= 3; // n = n - 3 → 12
n *= 2; // n = n * 2 → 24
n /= 4; // n = n / 4 → 6
n **= 2; // n = n ** 2 → 36
n %= 10; // n = n % 10 → 6
// Logical assignment operators (ES2021)
let a = null;
a ??= "default"; // assigns only if a is null/undefined → "default"
let b = 0;
b ||= 42; // assigns if b is falsy → 42
let c = 5;
c &&= c * 2; // assigns if c is truthy → 10
Comparison Operators
Strict equality (===) vs. loose equality (==)
Comparison operators evaluate a relationship between two values and return a boolean. The single most important comparison distinction in JavaScript is between strict equality (===) and loose equality (==). Strict equality checks both value and type with no conversion — it behaves predictably. Loose equality coerces types before comparing, which produces results that surprise almost every developer at some point.
// === checks value AND type — no coercion
1 === 1 // true
1 === "1" // false (number vs string)
null === null // true
null === undefined // false
// == coerces types before comparing — full of surprises
1 == "1" // true ← string coerced to number
0 == false // true ← false coerced to 0
0 == "" // true ← "" coerced to 0
"" == false // true
null == undefined // true ← the one useful == behavior
null == 0 // false ← null only == null or undefined
Rule of thumb: always use ===. The only exception is value == null to check for both null and undefined at once.
Relational operators
5 > 3 // true
5 < 3 // false
5 >= 5 // true
5 <= 4 // false
// String comparison is lexicographic (alphabetical by Unicode code point)
"banana" > "apple" // true
"10" > "9" // false ← "1" comes before "9" in Unicode
10 > 9 // true ← use numbers for numeric comparison
Logical Operators
AND (&&), OR (||), NOT (!)
Logical operators let you combine boolean expressions and are the backbone of conditional logic. Beyond their basic boolean use, && and || have a powerful behavior called short-circuit evaluation: they stop evaluating as soon as the result is determined, and they return the actual value that decided the outcome — not just true or false. This makes them useful for much more than simple boolean checks.
true && true // true
true && false // false
false || true // true
false || false // false
!true // false
!false // true
Short-circuit evaluation
// && returns the first falsy value, or the last value if all truthy
false && expensiveFunction() // expensiveFunction never called — short-circuits
"hello" && 42 // 42 (first is truthy, continues to second)
null && "anything" // null (first is falsy, stops here)
// || returns the first truthy value, or the last value if all falsy
true || expensiveFunction() // expensiveFunction never called — short-circuits
0 || "fallback" // "fallback" (0 is falsy, continues)
"exists" || "fallback" // "exists" (truthy, stops here)
// Practical: default value pattern (pre-ES2020 style)
const name = userInput || "Anonymous";
// Practical: guard against null before calling a method
user && user.save();
NOT (!) and double NOT (!!)
!0 // true
!"hello" // false
!!0 // false — double NOT converts any value to its boolean equivalent
!!"hello" // true
!!null // false
!![] // true — empty array is truthy
Nullish Coalescing (??)
Introduced in ES2020, ?? solves a real problem with ||: the fact that || falls back on any falsy value, including 0, "", and false — values that are perfectly valid in many situations. ?? is stricter: it only falls back when the left side is null or undefined. This makes it the right choice whenever you want to keep legitimate falsy values intact.
const userScore = 0;
// Wrong — || treats 0 as falsy, loses the real value
const display1 = userScore || "No score"; // "No score" ← bug!
// Correct — ?? only triggers on null/undefined
const display2 = userScore ?? "No score"; // 0 ← correct
// Real-world examples
const port = process.env.PORT ?? 3000; // keep 0 if PORT is "0"
const theme = user.preferences?.theme ?? "dark"; // default only if truly absent
const count = response.count ?? 0; // treat missing as zero, not falsy
Optional Chaining (?.)
When you access properties on deeply nested objects, any level that is null or undefined will throw a TypeError and crash your program. Optional chaining short-circuits the entire expression to undefined instead, letting you safely traverse structures where any part might be absent — common when working with API responses, user configuration, or optional features.
const user = {
name: "Alice",
address: {
city: "Tokyo"
}
};
// Without optional chaining — verbose and fragile
const city = user && user.address && user.address.city;
// With optional chaining — clean and safe
const city2 = user?.address?.city; // "Tokyo"
const zip = user?.address?.zip; // undefined (no error)
const first = user?.contacts?.[0]?.email; // undefined (no error)
// Works with method calls too
const upper = user?.name?.toUpperCase(); // "ALICE"
const result = obj?.method?.(); // calls method only if it exists
Combine with ?? for a safe default:
const city = user?.address?.city ?? "Unknown city";
Spread Operator (…)
The spread operator expands an iterable — an array, string, or object — into its individual elements in place. It’s one of the most versatile operators in modern JavaScript, used for copying arrays, merging objects, and passing variable-length argument lists to functions. Wherever you previously used concat, Object.assign, or apply, spread is usually cleaner.
// Merging arrays
const a = [1, 2, 3];
const b = [4, 5, 6];
const merged = [...a, ...b]; // [1, 2, 3, 4, 5, 6]
const copy = [...a]; // shallow copy — new array, same elements
// Merging objects (later keys win)
const defaults = { theme: "dark", lang: "en", timeout: 5000 };
const userPrefs = { theme: "light" };
const config = { ...defaults, ...userPrefs };
// { theme: "light", lang: "en", timeout: 5000 }
// Spreading into function arguments
const numbers = [5, 1, 8, 3];
Math.max(...numbers); // 8 — equivalent to Math.max(5, 1, 8, 3)
// Rest parameters — the reverse: collect remaining arguments into an array
function sum(first, ...rest) {
return rest.reduce((acc, n) => acc + n, first);
}
sum(1, 2, 3, 4); // 10
Ternary Operator
The ternary operator is a compact inline conditional: condition ? valueIfTrue : valueIfFalse. It’s ideal for simple two-branch choices that would be verbose as a full if/else — particularly inside template literals or JSX where a statement can’t appear. The key is to keep it simple: nesting ternaries quickly becomes unreadable, and a regular if/else or lookup object is almost always clearer for complex logic.
const age = 20;
const label = age >= 18 ? "Adult" : "Minor"; // "Adult"
// Practical: conditional class name
const className = isActive ? "btn btn-active" : "btn";
// Practical: pluralization in template literals
const msg = `You have ${count} ${count === 1 ? "message" : "messages"}`;
// Avoid nesting ternaries — it hurts readability
// Bad:
const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F";
// Better: use if/else or a lookup object for multiple branches
const getGrade = (score) => {
if (score >= 90) return "A";
if (score >= 80) return "B";
if (score >= 70) return "C";
return "F";
};
Operator Precedence Quick Reference
When operators are combined in a single expression, JavaScript evaluates them in a fixed order. When in doubt, use parentheses to make the intent explicit — they cost nothing and prevent subtle bugs.
// Precedence: ** > * / % > + - > comparisons > && > || > ??
2 + 3 * 4 // 14 (not 20 — multiplication before addition)
(2 + 3) * 4 // 20 — parentheses override precedence
true || false && false // true (&& has higher precedence than ||)
(true || false) && false // false — parentheses change the grouping
// ?? and || cannot be mixed without parentheses — this is a SyntaxError
// null ?? "a" || "b"
(null ?? "a") || "b" // "a"
null ?? ("a" || "b") // "a"
What’s Next
With operators covered, the next tutorial explores control flow — if/else, switch, loops, and how to direct your program’s execution based on conditions.