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)
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.
// 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)
The dangerous ones
A small set of precedence rules cause most of the surprise. Memorise these or always parenthesise.
Bitwise vs comparison
// 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
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
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
// ?: 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 &&
// 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
// 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
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 1Bug: new without parens
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, uglyComma — 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.
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
// 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();
}