The Operator Tour

Mental model: An operator is a function with punctuation for a name, and precedence decides who gets the operands.

Level: beginner · about 11 minutes

console.log(7 / 2);    // 3.5  there is no integer division
console.log(7 % 2);    // 1
console.log(-7 % 2);   // -1  the sign follows the left operand
console.log(7 % -2);   // 1   the right operand's sign is ignored
console.log(2 ** 10);  // 1024
console.log(2 ** -1);  // 0.5

Read the comments, then run it and check every line.

An operator takes one or more values (its operands) and produces a new value. That is all. The interesting part is not the list of operators, it is knowing which operator grabs which operand, and what each one does when the types are not what you expected.

Increment and decrement: where you put it matters

Postfix a++ returns the old value

let a = 5;
console.log(a++); // 5
console.log(a);   // 6

Prefix ++b returns the new value

let b = 5;
console.log(++b); // 6
console.log(b);   // 6

Both change the variable by one. They differ only in what the expression evaluates to. When the value is unused, as in a for header, the two are interchangeable, so prefer the form that reads better and never mix them into a larger expression.

Assignment operators

let n = 10;
n += 5;   // 15   shorthand for n = n + 5
n -= 3;   // 12
n *= 2;   // 24
n /= 4;   // 6
n %= 4;   // 2
n **= 3;  // 8
console.log(n); // 8

Logical assignment: &&=, ||=, ??=

const settings = { volume: 0, label: '' };

settings.volume ||= 50;    // 0 is falsy, so it is replaced
settings.label ??= 'none'; // '' is not nullish, so it survives
settings.mode ??= 'dark';  // undefined, so it is filled in

console.log(settings); // { volume: 50, label: '', mode: 'dark' }

Three operators, three different questions about the current value.

OperatorAssigns when the current value isEquivalent to
x &&= ytruthyx && (x = y)
x ||= yfalsy (0, '', false, null, undefined, NaN)x || (x = y)
x ??= ynullish only (null or undefined)x ?? (x = y)

Precedence and associativity

Precedence decides which operator binds tighter. Associativity decides what happens between two operators of equal precedence. You only need the rows beginners actually hit.

LevelOperatorsAssociativity
highest( ) groupingn/a
a.b a[b] f() new X()left to right
a++ a-- (postfix)n/a
! -a +a typeof void ++a --aright to left
**right to left
* / %left to right
+ -left to right
< <= > >= instanceof inleft to right
=== !== == !=left to right
&&left to right
|| and separately ??left to right
cond ? a : bright to left
= += &&= ||= ??=right to left
lowest, commaleft to right
console.log(2 + 3 * 4);              // 14, not 20
console.log(2 ** 3 ** 2);           // 512, because ** is right associative
console.log((2 ** 3) ** 2);         // 64
console.log(true || false && false); // true, && binds tighter than ||
console.log(-(2 ** 2));             // -4, parens required

Two operators you mostly read, rarely write

const x = (1, 2, 3);
console.log(x);            // 3, the comma operator evaluates all and yields the last

console.log(void 0);       // undefined, void discards its operand
console.log(typeof void 'anything'); // 'undefined'

for (let i = 0, j = 3; i < j; i++, j--) console.log(i, j); // 0 3 then 1 2
comma operator
evaluates left to right, returns the last value. Common in minified code and for headers.
`void 0`
a guaranteed undefined. It predates undefined being safe from reassignment.
`typeof`
a unary operator, not a function. typeof(x) just adds pointless parens.
let i = 1;
const result = i++ + ++i;
console.log(result, i);

i++ yields 1 and leaves i at 2. Then ++i raises i to 3 and yields 3. So result is 1 + 3, which is 4, and i is 3. This is also a good argument for never putting ++ inside a larger expression.

Try it yourself

Operator bench

const rows = [
  ['7 / 2',      7 / 2],
  ['7 % 2',      7 % 2],
  ['-7 % 2',     -7 % 2],
  ['2 ** 3 ** 2', 2 ** 3 ** 2],
  ['5 / 0',      5 / 0],
  ['0.1 + 0.2',  0.1 + 0.2],
];

for (const [label, value] of rows) console.log(label.padEnd(12), value);

Add rows of your own. Try 5 / 0, 0 / 0, 2 ** 53 + 1, and 0.1 + 0.2.

Exercises

A modulo that never goes negative

Write wrapIndex(index, length) that returns a position always inside 0 to length - 1, wrapping around in both directions. wrapIndex(-1, 3) is 2, wrapIndex(4, 3) is 1. Use the remainder operator, and remember its sign follows the left operand.

Check yourself

What does this log?
1 -1 — % returns a remainder whose sign matches the left operand, so -10 % 3 is -1. A true modulo would give 2. Use ((n % m) + m) % m when you need the always-positive version.
What is the value of 2 ** 3 ** 2?
512 — ** is right associative, so it groups as 2 ** (3 ** 2), which is 2 ** 9, which is 512. Almost every other binary operator groups left to right, which is why this one catches people.
config.retries is 0. After config.retries ||= 3, what is it?
3 — ||= assigns whenever the current value is falsy, and 0 is falsy, so a perfectly valid setting of zero retries gets overwritten. ??= only assigns for null and undefined, which is what you almost always want for configuration.

Common mistakes

  • Expecting % to behave like a mathematical modulo for negative numbers.
  • Using ||= or || for defaults when 0, '' or false are real values.
  • Burying ++ inside a bigger expression, then debugging the reading order instead of the logic.

Takeaways

  • % is a remainder: its sign follows the left operand.
  • ** is the only common operator that associates right to left.
  • &&=, ||= and ??= differ only in the question they ask about the current value.
  • When precedence is not obvious to a reader, add parentheses instead of a comment.