JavaScriptOperator Precedence

Operator Precedence

When several operators appear in the same expression, precedence decides who binds tighter, and associativity decides which side wins among operators of equal precedence. Most of the time, school-grade "multiplication before addition" intuition is right. The handful of cases where it isn't are where real-world bugs hide.

The short version

From highest precedence (binds tightest) to lowest (binds loosest), grouped into the categories you'll meet most often:

Practical precedence table (high → low)

Text
Grouping                    ( ... )
Member access               a.b   a?.b   a[b]
Function call / new         f()   new F(x)
Postfix increment           x++   x--
Logical NOT, bitwise NOT    !x    ~x
Unary plus/minus, typeof    +x    -x    typeof x   void x   delete x
Prefix increment            ++x   --x
Exponentiation              **            (right-associative)
Multiplicative              *  /  %
Additive                    +  -
Bitwise shift               <<  >>  >>>
Relational                  <  <=  >  >=  in  instanceof
Equality                    ==  !=  ===  !==
Bitwise AND                 &
Bitwise XOR                 ^
Bitwise OR                  |
Logical AND                 &&
Logical OR / Nullish        ||  ??
Conditional                 ? :
Assignment                  =  +=  -=  *=  /=  &&=  ||=  ??=
Comma                       ,

The MDN table is the authoritative source if you ever need the full ordering; the categories above are enough to read 99% of code.

Associativity — which side wins among equals

Most operators are left-associative — they group left to right. A few are right-associative, which is where surprises live.

JS
// Left-associative (most arithmetic and comparison)
1 + 2 + 3;           // (1 + 2) + 3
a - b - c;           // (a - b) - c

// Right-associative
2 ** 3 ** 2;         // 2 ** (3 ** 2) === 2 ** 9 === 512  (NOT (2**3)**2 = 64)
a = b = c = 0;       // a = (b = (c = 0))
cond ? x : flag ? y : z;  // cond ? x : (flag ? y : z)
Why ** is right-associative
Mathematically, `2^3^2` means `2^(3^2)` — that's the convention JavaScript follows. If you mean `(2**3)**2`, parenthesise.
The dangerous ones

A small set of precedence rules cause most of the surprise. Memorise these or always parenthesise.

Bitwise vs comparison

JS
// Bitwise & has LOWER precedence than ==
if (flags & 1 === 1) { /* probably wrong */ }
// Parses as: flags & (1 === 1) → flags & true → flags & 1 → 0 or 1

if ((flags & 1) === 1) { /* correct */ }

Logical AND/OR mix

JS
true || false && false;
// && binds tighter than ||, so:
// true || (false && false) → true || false → true

(true || false) && false;  // false — different result with explicit grouping

Assignment in conditions

JS
while (line = readNextLine()) { /* ... */ }
// = is the lowest-precedence non-comma operator, so the parens around (line = ...)
// are NOT strictly required, but most linters want them to flag intentional cases.

Ternary chaining

JS
// ?: is right-associative, so this works as a chain:
const tier =
  score >= 90 ? "gold"   :
  score >= 70 ? "silver" :
  score >= 50 ? "bronze" :
  /* else */     "none";

?? must be parenthesised with || or &&

JS
// Syntax error — explicitly disallowed:
// const v = a ?? b || c;
// const v = a || b ?? c;

// Both rewrites are fine:
const v1 = (a ?? b) || c;
const v2 = a ?? (b || c);
Real-world bug examples

Bug: typeof comparison

JS
// Looks right
if (typeof x == "number" || "string") { /* always true */ }

// Parses as: (typeof x == "number") || "string"
// The second operand is just the string "string" — truthy — so the whole thing is true.

if (typeof x === "number" || typeof x === "string") { /* correct */ }

Bug: increment + string concat

JS
const i = 0;
console.log("" + i++);  // "0"
// Postfix ++ binds tighter than +, so this is fine — but visually confusing.
// If `i` is a const, this also throws. Be explicit.

let j = 0;
console.log(`${j++}`); // "0", then j is 1

Bug: new without parens

JS
class Greeter { greet() { return "hi"; } }

new Greeter().greet();    // "hi"   — works
new Greeter.greet();      // TypeError — parses as new (Greeter.greet)()

// Explicit:
(new Greeter()).greet();
new (Greeter)().greet();   // also explicit, ugly
`new` is the one that catches everyone
`new` with arguments binds tighter than `.` — but `new` *without* arguments binds **looser**. `new Greeter.greet()` tries to construct `Greeter.greet`, not call `.greet()` on the new instance. Always include the parens after the constructor.
Comma — the lowest-precedence operator

The comma operator evaluates its operands left to right and returns the last one. It has lower precedence than =, which is why you almost never see it without parentheses.

JS
let x = (1, 2, 3);   // x is 3 — the value of the last operand
let y =  1, 2, 3;    // SyntaxError — without parens, comma is a declaration separator

for (let i = 0, j = 10; i < j; i++, j--) {
  // here the commas in the init are part of the let-declaration syntax,
  // and the commas in the update are the comma operator
}
A practical rule
  • Don't memorise the full table. Recognise the categories and parenthesise across them.

  • Always parenthesise mixed && and ||, mixed ?? with anything, and bitwise mixed with comparison.

  • Always parenthesise the construction in new Foo().bar() if it would otherwise read ambiguously.

  • Trust your editor's syntax highlighting — it won't catch precedence bugs, but Prettier reformatting can show you what the parser thinks.

One safe expression at a time

Refactor for clarity

JS
// Unclear — has a real bug
if (flags & READ === 0 || flags & WRITE === 0) {
  block();
}

// Step 1: parenthesise the bitwise pieces
if ((flags & READ) === 0 || (flags & WRITE) === 0) {
  block();
}

// Step 2: name the intent
const canRead  = (flags & READ)  !== 0;
const canWrite = (flags & WRITE) !== 0;
if (!canRead || !canWrite) {
  block();
}
Parentheses are not a code smell
When experienced developers write extra parentheses, they're usually documenting intent: "this is how I read this expression, and I want you to read it the same way." Don't strip parens that exist for clarity — even if the parser doesn't need them.