JavaScript Operators

Operators are the symbols and keywords JavaScript uses to actually do things with values: add two numbers, compare a user’s age to a limit, combine several conditions into one, or fall back to a default when something might be missing. Almost every useful line of JavaScript contains at least one operator, so understanding exactly how each one evaluates — including the type coercion quirks that catch even experienced developers off guard — is one of the highest-leverage topics in the language. This lesson walks through every major category of operator, explains how the JavaScript engine evaluates them internally, and covers the classic mistakes you should learn to avoid.

Overview / How Operators Work

An operator is a special symbol or keyword that takes one or more operands (the values it acts on) and produces a resulting value. Operators are classified by how many operands they take:

  • Unary operators act on a single operand, e.g. -x, !isValid, typeof x.
  • Binary operators act on two operands, e.g. a + b, a && b. Most operators in JavaScript are binary.
  • Ternary — there is exactly one: the conditional operator condition ? valueIfTrue : valueIfFalse, which takes three operands.

Because JavaScript is dynamically typed, the engine often has to decide what type an operand “should” be before it can apply an operator. This process is called type coercion. For example, when you write "5" - 2, the engine converts the string "5" to the number 5 before subtracting, producing 3. But "5" + 2 behaves completely differently — because + also means string concatenation, JavaScript converts the number 2 to the string "2" instead, producing "52". This asymmetry — + prefers strings, every other arithmetic operator prefers numbers — is one of the most important rules to internalize.

Every expression built from operators is evaluated according to two rules: precedence (which operator binds tighter, similar to “multiplication before addition” in math class) and associativity (when two operators have equal precedence, does evaluation go left-to-right or right-to-left?). Most operators are left-associative, but assignment operators and the ternary operator are right-associative, which is why let a = b = 5; works: b = 5 is evaluated first (and itself evaluates to 5), then that result is assigned to a.

Two operator families — &&, ||, and ?? — additionally use short-circuit evaluation: the right-hand operand is only evaluated if it’s actually needed. a && b returns a immediately without touching b if a is falsy; a || b returns a immediately if it’s truthy. This isn’t just an optimization — it’s routinely used as a control-flow pattern, such as user && user.logout() to call a method only if user exists.

Syntax

The table below summarizes the main operator categories you’ll use constantly:

Category Operators Example Notes
Arithmetic + - * / % ** total * 1.08 % is remainder, ** is exponent
Assignment = += -= *= /= %= **= score += 10 Shorthand for score = score + 10
Comparison == === != !== > < >= <= age >= 18 ===/!== compare type and value
Logical && || ! isAdult && hasID Short-circuiting, return an operand (not always boolean)
Ternary ? : age >= 18 ? "adult" : "minor" Inline if/else as an expression
Nullish coalescing ?? input ?? "default" Falls back only for null/undefined, not 0 or ""
Optional chaining ?. user?.address?.city Short-circuits to undefined instead of throwing
Type typeof, instanceof typeof 5 Inspect a value’s type
Bitwise & | ^ ~ << >> >>> flags & 1 Operates on 32-bit integer representation, rarely needed day-to-day

Examples

Example 1: Arithmetic and Assignment Operators

let price = 20;
let quantity = 3;
let total = price * quantity;
total += 5; // add a flat shipping fee
const isExpensive = total > 50;

console.log(total);
console.log(isExpensive);
console.log(10 % 3);
console.log(2 ** 8);

Output:

65
true
1
256

This example chains familiar arithmetic operators with the compound assignment operator +=, which is shorthand for total = total + 5. quantity and price are multiplied first (precedence gives * priority), then total is reassigned with the shipping fee added. 10 % 3 demonstrates the remainder operator (10 divided by 3 leaves a remainder of 1), and 2 ** 8 demonstrates exponentiation (2 raised to the 8th power).

Example 2: Comparison, Logical Operators, and Equality Pitfalls

const age = 20;
const hasID = true;

console.log(age >= 18 && hasID);
console.log(age < 18 || !hasID);

console.log(0 == "0");
console.log(0 === "0");
console.log(null == undefined);
console.log(null === undefined);

const user = null;
const name = user?.profile?.name ?? "Guest";
console.log(name);

Output:

true
false
true
false
true
false
Guest

The first two lines show && and || combining boolean comparisons. The middle four lines contrast loose equality (==), which converts operands to a common type before comparing, with strict equality (===), which never converts — 0 == "0" is true because the string is coerced to a number, but 0 === "0" is false because the types differ. null and undefined are a special case: they’re loosely equal to each other but not strictly equal. The last line uses optional chaining (?.) to safely read a nested property that doesn’t exist (because user is null) without throwing, and nullish coalescing (??) to substitute "Guest" since the result was undefined.

Example 3: The Ternary Operator and Precedence

function classify(score) {
  return score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F";
}

let attempts = 0;
attempts += 1;
attempts *= 2;

console.log(classify(95));
console.log(classify(82));
console.log(classify(50));
console.log(attempts);
console.log(typeof attempts);
console.log(1 + 2 * 3);
console.log((1 + 2) * 3);

Output:

A
B
F
2
number
7
9

classify chains three ternary operators to pick a letter grade; because ?: is right-associative, this reads correctly as nested if/else-if checks. attempts shows compound assignment operators running in sequence: it starts at 0, becomes 1 after += 1, then 2 after *= 2. The last two lines show precedence in action: multiplication binds tighter than addition, so 1 + 2 * 3 computes 2 * 3 first (giving 7), while explicit parentheses in (1 + 2) * 3 force the addition to happen first (giving 9).

Under the Hood

When the JavaScript engine parses an expression like a + b * c, it doesn’t evaluate left to right character by character. It first builds an internal tree (an abstract syntax tree) shaped by operator precedence, so b * c becomes a sub-expression that is evaluated before being added to a. Only once the tree is built does the engine start evaluating from the innermost operations outward.

For binary operators, both operands are generally evaluated first (left operand, then right operand), and only then is the operator applied. This matters when operands have side effects, such as function calls that log something or mutate a variable — the left side’s side effects always happen before the right side’s, except when short-circuiting prevents the right side from running at all.

Loose equality (==) follows a documented algorithm called the Abstract Equality Comparison. Roughly: if both operands are the same type, it behaves exactly like ===. If the types differ, JavaScript converts one or both operands (numbers and strings convert toward numbers, booleans convert to numbers, objects convert via valueOf/toString) and compares again. null and undefined are only loosely equal to each other and to nothing else. Because this algorithm has surprising edge cases (like [] == false being true), most style guides recommend avoiding == entirely in favor of ===.

Assignment operators (=, +=, etc.) are themselves expressions — they evaluate to the value that was assigned. This is why if (isAdmin = true) is legal JavaScript: it assigns true to isAdmin and the if then receives that assigned value. It’s rarely what you meant to write, which brings us to the mistakes below.

Common Mistakes

Mistake 1: Using = Instead of === in a Condition

Because a single = is valid inside an if condition (it’s just an assignment expression), a typo can silently change your program’s logic instead of throwing an error:

let isAdmin = false;

if (isAdmin = true) {
  console.log("Access granted");
}

Output:

Access granted

This always logs "Access granted", no matter what isAdmin was before, because the condition assigns true and then evaluates to true. The fix is to use the comparison operator ===:

let isAdmin = false;

if (isAdmin === true) {
  console.log("Access granted");
} else {
  console.log("Access denied");
}

Output:

Access denied

Mistake 2: Trusting Loose Equality (==)

Loose equality’s type coercion produces results that look wrong at a glance:

console.log("" == 0);
console.log(" " == 0);
console.log([] == 0);
console.log(null == 0);

Output:

true
true
true
false

An empty string, a whitespace-only string, and an empty array are all loosely equal to 0 because they’re each coerced to the number 0 before comparing — yet null, which “feels” empty too, is false because null only loosely equals undefined and itself. Switching to strict equality removes all the coercion and gives predictable results:

console.log("" === 0);
console.log(" " === 0);
console.log([] === 0);
console.log(null === 0);

Output:

false
false
false
false

Best Practices

  • Default to === and !==. Only reach for == when you specifically want null/undefined to be treated as equal.
  • Use ?? instead of || when falling back for “missing” values, since || also replaces legitimate falsy values like 0, "", or false.
  • Reach for ?. when reading properties that might not exist, instead of writing long && chains like user && user.profile && user.profile.name.
  • Add parentheses around mixed logical expressions (e.g. combining && and ||) even when not strictly required — it removes any doubt about precedence for whoever reads the code next.
  • Avoid chained assignments like let a = b = c = 0; in real code; they’re compact but make it easy to accidentally create an undeclared global if one variable isn’t declared.
  • Use Number.isNaN(x) rather than x === NaN, since NaN is never equal to anything, including itself.
  • Prefer compound assignment operators (+=, *=, etc.) over verbose repetition — they’re idiomatic and reduce the chance of typos in the variable name.

Practice Exercises

Exercise 1: Write a function isEven(n) that uses the remainder operator (%) to return true if n is even and false otherwise.

Exercise 2: Given const config = { timeout: 0 };, write an expression using ?? that reads config.timeout and falls back to 5000 only if timeout is missing. Then write a second expression using || for the same thing, run both, and explain why they produce different results.

Exercise 3: Rewrite the following nested if/else chain as a single ternary expression: if temp > 30 log "hot", else if temp > 15 log "mild", else log "cold".

Summary

  • Operators combine one, two, or three operands into a resulting value; they can be unary, binary, or (for the ternary operator) three-part.
  • Arithmetic operators coerce operands toward numbers, except +, which prefers strings if either operand is a string.
  • Always prefer ===/!== over ==/!= to avoid unpredictable type coercion.
  • &&, ||, and ?? short-circuit — the right operand only runs when needed — which makes them useful for control flow, not just booleans.
  • ?? only falls back for null/undefined; || falls back for any falsy value.
  • Operator precedence and associativity determine evaluation order in complex expressions; use parentheses to make intent explicit.
  • Assignment expressions return the assigned value, which is why = inside a condition is a legal but dangerous typo for ===.