Operators

Use the operators that make code shorter without making it obscure.

06of 32
≈ 9Min read
9Parts

Arithmetic

% is a remainder, not a modulo. -1 % 12 is -1, not 11, which breaks clock and wrap-around arithmetic until you write the helper above.

including the two people forget
a + b    a - b    a * b    a / b
a % b     // remainder — the SIGN follows a, so -7 % 3 is -1
a ** b    // power: 2 ** 10 is 1024

// a true modulo, always positive
const mod = (a, n) => ((a % n) + n) % n;

Assignment shorthands

all of them combine with =
total += 10;     total -= 10;
total *= 2;      total /= 2;
total %= 12;     total **= 2;

// the logical ones
config.retries ||= 3;    // assign if falsy   — 0 would be replaced
config.retries ??= 3;    // assign if null/undefined — 0 is kept
loading &&= false;       // assign only if currently truthy
||= or ??=

Use ??= for defaults. ||= replaces 0, "" and false, which is almost never what a default is meant to do.

Comparison

The relational operators have no strict version. "10" < "9" is true because both are strings and they compare character by character. Convert first.

strict, always
a === b    a !== b     // no coercion — use these
a == b     a != b      // coerces — avoid, except x == null

a < b   a > b   a <= b   a >= b   // these DO coerce, always

Logical operators return a value, not a boolean

this is what makes them useful
a && b    // if a is falsy, return a; otherwise return b
a || b    // if a is truthy, return a; otherwise return b
a ?? b    // if a is null or undefined, return b; otherwise a
!a        // the only one that really returns a boolean

"" || "fallback"      // "fallback"
0 || 10               // 10        ← probably a bug
0 ?? 10               // 0         ← what you meant
null ?? 10            // 10

user && user.name     // undefined-safe, the old way
user?.name            // the modern way

Short circuit

Short circuiting is a control-flow tool wearing an expression's clothes. It is excellent for defaults and terrible for side effects — a statement that only sometimes runs should look like a statement.

the right-hand side may never run
// the call only happens if the check passes
isReady && start();

// a guard that reads well
const name = user?.name ?? "Anonymous";

// and the misuse to avoid
count || setCount(1);   // fires when count is 0 — use an if

Optional chaining

three forms
user?.address?.city        // property
list?.[0]                  // index
callback?.()               // call — only if it is a function

// it stops the WHOLE chain, it does not make errors go away
user?.address.city         // still throws if address is undefined
Do not scatter it

a?.b?.c?.d everywhere hides the real question: why might these be missing? Use it at the one boundary where a value is genuinely optional, and be confident about the rest.

The ternary

an expression, so it goes anywhere
const label = days > 7 ? "slow" : "quick";

`${count} ${count === 1 ? "door" : "doors"}`

// nested once is readable
const size = w > 1200 ? "large" : w > 700 ? "medium" : "small";

// twice is not — use a lookup or an if

Comma, void, and the ones to know but not write

you will meet these in other people's code
(0, eval)("x")     // indirect eval — runs in global scope
void 0             // undefined, from before undefined was safe
a, b               // evaluates both, returns b
~n                 // bitwise NOT; ~str.indexOf(x) was the old "includes"
n | 0              // truncate to a 32-bit integer — use Math.trunc
n >>> 0            // unsigned 32-bit

Precedence

Learn three rules and parenthesise the rest: * before and /, which come before + and -; comparisons come before &&, which comes before ||; assignment is last.

Mixing ?? with && or || is a syntax error without parentheses — the language forces you to be explicit there, and it is right to.

the enforced one
a ?? b || c        // SyntaxError
(a ?? b) || c      // fine
a ?? (b || c)      // fine

Practice