Skip to main content

๐Ÿงฎ Operator Precedence and Expressions

When you write 2 + 3 * 4, JavaScript has to decide what runs first. The rules that make that decision โ€” precedence and associativity โ€” are the same ones that cause a whole class of silent, hard-to-spot bugs. This lesson makes those rules visible and teaches you to write expressions no one has to decode.

๐ŸŽฏ Learning Objectives

By the end of this lesson, you will be able to:

  • Define an expression and list the common kinds you write every day
  • Use a precedence table to predict the evaluation order of a mixed expression
  • Distinguish left- from right-associative operators (assignment and **)
  • Reach for parentheses to make intent explicit and override defaults
  • Recognise and fix the classic precedence pitfalls โ€” && before ||, = vs ===, and side effects in expressions

Estimated Time: 30โ€“40 minutes  โ€ข  Difficulty: Beginnerโ€“Intermediate

Hands-on: Predict outputs, then hunt and fix a real precedence bug in an authorization check.

In This Lesson

What Is an Expression?

An expression is any piece of code that produces a value. That's the whole definition โ€” and it covers a lot of ground:

KindExample
Literal42, "Hello", true
VariableuserName
Arithmeticprice * quantity
String"Hi, " + name
Logicalage >= 18 && hasID
Assignmentx = 5 (evaluates to 5)
Function callMath.max(a, b)

Expressions nest: price * quantity + tax is an expression made of smaller expressions. The moment you combine operators, JavaScript needs rules to decide the order โ€” and that's what precedence and associativity are.

๐Ÿ’ก Analogy โ€” a recipe. A chef follows an order: cream the butter and sugar before folding in flour. Swap the steps and you get a different cake. JavaScript follows precedence the same way โ€” combine the same ingredients in a different order and you get a different result.

Operator Precedence

Precedence decides which operator binds more tightly when several appear in one expression. Higher precedence runs first โ€” exactly like "multiplication before addition" from school math.

Here is a working subset, from highest to lowest. You don't need to memorise all of it; you need to know the shape and where the surprises live.

TierOperatorsAssociativity
Grouping( โ€ฆ )n/a
Access / callobj.prop, arr[i], fn()left โ†’ right
Postfixx++, x--n/a
Unary / prefix!x, -x, +x, ++x, typeofright โ†’ left
Exponentiation**right โ†’ left
Multiplicative*, /, %left โ†’ right
Additive+, -left โ†’ right
Relational<, <=, >, >=, instanceofleft โ†’ right
Equality===, !==, ==, !=left โ†’ right
Logical AND&&left โ†’ right
Logical OR||left โ†’ right
Nullish??left โ†’ right
Conditional? :right โ†’ left
Assignment=, +=, *=, โ€ฆright โ†’ left
console.log(2 + 3 * 4);   // 14 โ€” * binds tighter than +
console.log((2 + 3) * 4); // 20 โ€” parentheses win

console.log(5 > 3 && 2 < 4); // true โ€” comparisons run before &&
console.log(-2 * 4);          // -8  โ€” unary - binds before *

// The one everyone forgets: && is higher than ||
console.log(true || false && false); // true โ€” reads as true || (false && false)

โš ๏ธ The three you must remember

Most precedence surprises come from just three facts: (1) && is higher than ||; (2) assignment is lower than almost everything; (3) ** and assignment are right-associative. Everything else you can look up or parenthesise.

Associativity

When two operators have the same precedence, associativity breaks the tie. Most operators are left-associative (evaluated left to right); a few important ones โ€” assignment and exponentiation โ€” are right-associative.

// Left-associative: subtraction and division go left โ†’ right
console.log(10 - 5 - 2); // 3   โ†’ ((10 - 5) - 2)
console.log(20 / 5 / 2); // 2   โ†’ ((20 / 5) / 2)

// A subtle trap: relational operators are left-associative too
console.log(3 > 2 > 1);   // false! โ†’ (3 > 2) > 1 โ†’ true > 1 โ†’ 1 > 1 โ†’ false

// Right-associative: assignment chains resolve right โ†’ left
let a, b, c;
a = b = c = 5;           // c = 5, then b = 5, then a = 5

// Right-associative: exponentiation
console.log(2 ** 3 ** 2); // 512 โ†’ 2 ** (3 ** 2) = 2 ** 9
graph TD A["a = b = c = 5"] A --> B["Step 1: c = 5 (rightmost first)"] B --> C["Step 2: b = result of step 1"] C --> D["Step 3: a = result of step 2"]
๐Ÿ’ก Analogy โ€” assembly-line direction. Left-associative is a standard line moving left to right; each station takes the piece from its left. Right-associative runs in reverse โ€” a station waits for the one on its right to finish before it acts. Assignment chains work that way: the far-right value is produced first.

Grouping with Parentheses

Parentheses have the highest precedence of all. They do two jobs: they override the default order, and โ€” just as importantly โ€” they document your intent so the next reader (often future-you) doesn't have to recall the whole table.

// Override the default order
console.log(2 + 3 * 4);     // 14
console.log((2 + 3) * 4);   // 20

console.log(true || false && false);   // true
console.log((true || false) && false); // false โ€” forces || first

// Document intent, even when not strictly required
const a = 5, b = 10, c = 15;
const clear = a + (b * c) - (a / b); // identical result, obvious reading

โœ… The golden rule

When you mix operators from different families in one expression, add parentheses. They cost nothing at runtime and save the reader from mentally running the precedence table. "Would a teammate have to look this up?" โ€” if yes, parenthesise.

Expressions & Side Effects

An expression's job is to produce a value, but some expressions also change the world along the way โ€” that change is a side effect. Assignment, increment/decrement, and function calls can all carry side effects.

// Assignment is an expression AND a side effect
let x = 5;
let y = (x = 10); // side effect: x becomes 10; value: 10
console.log(x, y); // 10 10

// Increment side effects depend on prefix vs postfix
let counter = 0;
const sum = increment() + increment() + increment();
function increment() { counter += 1; return counter; }
console.log(counter); // 3
console.log(sum);     // 1 + 2 + 3 = 6
๐Ÿ’ก Analogy โ€” ripples in a pond. The splash is the expression's value; the ripples spreading outward are the side effects. Ripples can reach shores you didn't intend to touch โ€” which is why hiding side effects inside a big expression makes bugs hard to trace.

โš ๏ธ Order-dependent, reader-hostile

Expressions like i++ + ++j or getValue() + (counter++) * update(x) mix value and side effect so tightly that the outcome depends on exact evaluation order. They're legal, but they invite bugs. Pull side effects onto their own lines.

Precedence Pitfalls

These are the mistakes precedence actually causes in production code. Seeing them once makes them easy to catch in review.

1. && binds tighter than ||

let name = '';
const allowEmpty = false;
const defaultName = 'Guest';

// Intended: "use name, or if empty fall back to Guest"
let displayName = name || allowEmpty && defaultName;
// Actually parsed as: name || (allowEmpty && defaultName)
// โ†’ '' || (false && 'Guest') โ†’ '' || false โ†’ false
console.log(displayName); // false โ€” a bug!

// Fix with parentheses (or better, restructure)
displayName = name || defaultName;
console.log(displayName); // 'Guest'

2. = (assign) where you meant === (compare)

let x = 5;
if (x = 10) {           // assigns 10, which is truthy โ€” always runs
  console.log('oops');  // this line always executes, and x is now 10
}
// Meant: if (x === 10) { ... }

3. Relational chaining doesn't mean what math means

// You cannot write "is 5 between 1 and 10" as 1 < 5 < 10 directly
console.log(1 < 5 < 10);  // true, but by accident: (1 < 5) < 10 โ†’ true < 10 โ†’ 1 < 10
console.log(10 < 5 < 1);  // ALSO true! (10 < 5) < 1 โ†’ false < 1 โ†’ 0 < 1
// Correct range check:
const x2 = 5;
console.log(x2 > 1 && x2 < 10); // true, and actually correct

๐Ÿž Worked example โ€” the authorization bug

This real-world flavour of pitfall #1 hands out access it shouldn't:

// BUGGY: precedence makes this "(user && user.isActive) || (user.isAdmin && resource.isPublic)"
function canAccess(user, resource) {
  return user && user.isActive || user.isAdmin && resource.isPublic;
}
// An active regular user passes the first clause and gets into PRIVATE resources.

// FIXED: parenthesise to state the real rule
function canAccessFixed(user, resource) {
  return Boolean(user) &&
         (user.isActive || user.isAdmin) &&
         (user.isAdmin || resource.isPublic);
}

const active = { isActive: true, isAdmin: false };
const privateDoc = { isPublic: false };
console.log(canAccess(active, privateDoc));      // true  โ† leak
console.log(canAccessFixed(active, privateDoc)); // false โ† correct

Refactoring for Clarity

The cure for a scary expression is rarely "more parentheses" alone โ€” it's naming the pieces. Break a compound condition into well-named variables or small functions, and precedence stops mattering because each step is simple.

// Hard to audit at a glance
const allowed = user && user.roles &&
  (user.roles.includes('admin') || user.roles.includes('editor')) &&
  (resource.isPublic || resource.ownerId === user.id);

// Refactored: each line is a plain-English claim
const canEdit  = user?.roles?.includes('admin') || user?.roles?.includes('editor');
const owns     = resource.isPublic || resource.ownerId === user?.id;
const allowed2 = Boolean(user) && canEdit && owns;

The same idea scales to arithmetic. A pricing formula crammed into one line becomes obvious when each concept gets a function:

function discounted(price, percent) { return price - price * (percent / 100); }
function withTax(price, rate)        { return price * (1 + rate / 100); }
function shipping(subtotal, free, cost) { return subtotal >= free ? 0 : cost; }

const net   = discounted(subtotal, 10);
const total = withTax(net, 8) + shipping(subtotal, 50, 5.99);

โœ… Four habits for complex expressions

  • Parenthesise across operator families.
  • Name subexpressions with descriptive variables.
  • Isolate side effects onto their own lines.
  • Extract repeated or gnarly logic into small functions.

Hands-on Exercise

๐Ÿ‹๏ธ Predict, then fix

Part A โ€” predict the output. Write down your answer for each line before running it:

let a = 5, b = 10, c = 15;
console.log(a + b * c);          // ?

let x = 20, y = 10;
console.log(x / y + y / x * 2);  // ?

const m = true, n = false, p = true;
console.log(m && n || p);         // ?

let count = 0;
console.log(count++ + ++count + count++); // ?
console.log(count);                        // ?

Part B โ€” fix the bug. This discount helper caps the discount but returns the wrong value. Find why and fix it:

function cappedDiscount(price, percent, max) {
  return price - price * percent / 100 > max ? max : price * percent / 100;
}
๐Ÿ’ก Hint

Part A: apply precedence (*// before +; && before ||) and remember postfix returns the old value while prefix returns the new one. Part B: the condition computes price - (price*percent/100) โ€” the price after discount โ€” but then compares that against max, which isn't the discount amount at all.

โœ… Answers & solution
// Part A
a + b * c            // 155  โ†’ 5 + (10 * 15)
x / y + y / x * 2    // 3    โ†’ (20/10) + ((10/20) * 2) = 2 + 1
m && n || p          // true โ†’ (true && false) || true
count++ + ++count + count++ // 0 + 2 + 2 = 4
count                       // 3

// Part B โ€” name the discount, compare the right thing
function cappedDiscount(price, percent, max) {
  const discount = price * percent / 100;
  return discount > max ? max : discount;
}
console.log(cappedDiscount(200, 30, 50)); // 50  (30% = 60, capped at 50)
console.log(cappedDiscount(200, 10, 50)); // 20  (10% = 20, under the cap)

Note: many people predict count++ + ++count + count++ as 4 with a final count of 3 โ€” walking it step by step (0, then 2, then 2; increments applied) is the reliable way, and the fact that it's this fiddly is exactly why you avoid such expressions in real code.

๐ŸŽฏ Quick Quiz

Question 1: What does 2 + 3 * 4 evaluate to?

Question 2: Because && has higher precedence than ||, how does JavaScript read a || b && c?

Question 3: Which operator is right-associative, making 2 ** 3 ** 2 equal 512?

Summary & Quiz

๐ŸŽ‰ Key Takeaways

  • An expression is any code that produces a value; combining them invokes precedence rules.
  • Precedence decides which operator runs first โ€” * before +, && before ||, assignment last.
  • Associativity breaks ties: most operators go leftโ†’right, but = and ** go rightโ†’left.
  • Parentheses both override the order and document intent โ€” use them across operator families.
  • Watch the classic pitfalls (&&/||, = vs ===, hidden side effects) and refactor by naming the pieces.

๐Ÿ“š Further Reading

๐Ÿš€ What's Next?

You can now read and write any expression with confidence. Next we use them to steer a program's flow in Conditional Statements (if, else, switch), where these Boolean expressions finally decide which code runs.

๐ŸŽ‰ Expression mastery unlocked!

No more guessing what runs first. Time to make your programs branch.