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.
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
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
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.
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
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 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
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
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
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
(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.
a ?? b || c // SyntaxError
(a ?? b) || c // fine
a ?? (b || c) // fine