Operators and Control Flow
Operators combine values; control flow decides which code runs. JavaScript’s operators are mostly conventional — the interesting parts are coercion, the difference between == and ===, and the nullish family added in ES2020.
Arithmetic operators
Section titled “Arithmetic operators”7 + 2; // => 97 - 2; // => 57 * 2; // => 147 / 2; // => 3.5 (always float division — no integer division)7 % 2; // => 1 (remainder)7 ** 2; // => 49 (exponentiation, ES2016)% is a remainder, not a modulo: it keeps the sign of the left operand.
-7 % 3; // => -1 (not 2)((-7 % 3) + 3) % 3; // => 2 (true modulo)Increment and decrement come in prefix and postfix forms. Prefix returns the new value; postfix returns the old one.
let n = 5;n++; // => 5, n is now 6++n; // => 7, n is now 7Compound assignment exists for every arithmetic operator: +=, -=, *=, /=, %=, **=.
+ is overloaded: if either operand is a string, it concatenates. Every other arithmetic operator converts to number.
'5' + 3; // => '53''5' - 3; // => 2'5' * '2'; // => 10true + 1; // => 2 (true coerces to 1)Comparison operators
Section titled “Comparison operators”1 < 2; // => true2 <= 2; // => true3 > 4; // => false'a' < 'b'; // => true (lexicographic by UTF-16 code unit)Relational operators on strings compare code units, so 'Z' < 'a' is true and '10' < '9' is true. For human-readable ordering use localeCompare:
['10', '9', '2'].sort(); // => ['10', '2', '9']['10', '9', '2'].sort((a, b) => a - b); // => ['2', '9', '10']['ä', 'a', 'z'].sort((a, b) => a.localeCompare(b, 'de')); // => ['a', 'ä', 'z']Any comparison involving NaN is false, including NaN <= NaN.
== vs ===
Section titled “== vs ===”=== (strict equality) compares type and value with no conversion. == (loose equality) converts operands to a common type first.
Use === always. The one accepted exception is x == null to test for null-or-undefined.
1 === '1'; // => false1 == '1'; // => true
0 == false; // => true'' == false; // => true[] == false; // => true ← [] → '' → 0, false → 0null == undefined; // => truenull == false; // => false ← null only equals undefinedThe coercion rules behind ==
Section titled “The coercion rules behind ==”When the types differ, == applies these steps in order:
null == undefined→true. Neither is loosely equal to anything else.number == string→ convert the string to a number.boolean == anything→ convert the boolean to a number (true→1,false→0), then retry.object == primitive→ convert the object to a primitive (viavalueOf, thentoString), then retry.bigint == number/string→ compare mathematical values.
Walking through [] == false:
[] == false[] == 0 (boolean → number)'' == 0 (array → primitive: [].toString() is '')0 == 0 (string → number)trueAnd the famous non-transitive triple:
'0' == false; // => true ('0' → 0, false → 0)'0' == 0; // => truefalse == 0; // => true'0' == ''; // => false (both strings, compared directly)Object.is
Section titled “Object.is”Object.is is === with two corrections: NaN equals itself, and +0 does not equal -0.
NaN === NaN; // => falseObject.is(NaN, NaN); // => trueObject.is(0, -0); // => falseObjects compare by identity
Section titled “Objects compare by identity”=== on objects asks “is it the same object?”, never “does it have the same contents?”.
{ a: 1 } === { a: 1 }; // => falseconst x = { a: 1 };const y = x;x === y; // => trueFor structural comparison, compare the fields you care about, or JSON.stringify both (fragile — key order matters and it fails on undefined, functions, Map, and cycles).
Truthy and falsy
Section titled “Truthy and falsy”Every value converts to a boolean in a condition. There are exactly eight falsy values; everything else is truthy.
false 0 -0 0n '' null undefined NaNif ([]) console.log('empty array is truthy'); // printsif ({}) console.log('empty object is truthy'); // printsif ('0') console.log('the string "0" is truthy'); // printsif (0) console.log('never');Logical operators
Section titled “Logical operators”&& and || short-circuit and return one of their operands, not a boolean.
'a' && 'b'; // => 'b' (both truthy → the last one)'' && 'b'; // => '' (stops at the first falsy)'' || 'b'; // => 'b' (first truthy)null || 0 || 'found'; // => 'found'!'x'; // => false (! always yields a real boolean)This drives two idioms:
// Conditional executionisReady && start();
// Fallback valueconst port = config.port || 3000;The || fallback has a bug: it fires on any falsy value, so config.port = 0 becomes 3000. That is exactly what ?? fixes.
Nullish coalescing: ??
Section titled “Nullish coalescing: ??”?? returns the right side only when the left is null or undefined. Zero, empty string, and false pass through.
0 || 'default'; // => 'default' ← usually wrong0 ?? 'default'; // => 0 ← usually right
'' ?? 'default'; // => ''null ?? 'default'; // => 'default'undefined ?? 'default'; // => 'default'Optional chaining: ?.
Section titled “Optional chaining: ?.”?. stops evaluating and returns undefined if the value to its left is null or undefined, instead of throwing.
const user = { profile: { name: 'Ada' } };
user.profile.name; // => 'Ada'user.settings.theme; // TypeError: Cannot read properties of undefineduser.settings?.theme; // => undefinedThree forms:
obj?.prop // property accessobj?.[key] // computed accessobj.method?.() // call only if the method existsconst config = null;config?.get?.('key'); // => undefined, no errorIt combines naturally with ??:
const theme = user.settings?.theme ?? 'light';Logical assignment operators
Section titled “Logical assignment operators”ES2021 added three operators that assign only when the short-circuit says to.
a ||= b; // a = a || b — assign if a is falsya &&= b; // a = a && b — assign if a is truthya ??= b; // a = a ?? b — assign if a is null or undefinedThey short-circuit, so the right side is not evaluated (and the property is not written) when the condition fails — which matters for setters and proxies.
const opts = { retries: 0, label: '' };opts.retries ??= 3; // stays 0opts.label ||= 'n/a'; // becomes 'n/a'opts.timeout ??= 500; // added: 500// => { retries: 0, label: 'n/a', timeout: 500 }Bitwise operators
Section titled “Bitwise operators”These convert operands to 32-bit signed integers, operate bit by bit, and convert back to a double.
| Operator | Meaning | Example |
|---|---|---|
& |
AND | 12 & 10 → 8 |
| |
OR | 12 | 10 → 14 |
^ |
XOR | 12 ^ 10 → 6 |
~ |
NOT | ~12 → -13 |
<< |
left shift | 1 << 4 → 16 |
>> |
signed right shift | -16 >> 2 → -4 |
>>> |
unsigned right shift | -16 >>> 28 → 15 |
// Flagsconst READ = 1, WRITE = 2, EXEC = 4;let perms = READ | WRITE; // => 3(perms & WRITE) !== 0; // => trueperms &= ~WRITE; // remove WRITE => 1Ternary operator
Section titled “Ternary operator”condition ? whenTrue : whenFalse — the only three-operand operator, and an expression, so it produces a value.
const label = count === 1 ? 'item' : 'items';const tier = n > 100 ? 'gold' : n > 50 ? 'silver' : 'bronze';if / else if / else
Section titled “if / else if / else”const status = 404;
if (status < 300) { console.log('ok');} else if (status < 400) { console.log('redirect');} else if (status < 500) { console.log('client error');} else { console.log('server error');}Braces are optional for single statements but omitting them causes real bugs. Always use them.
switch
Section titled “switch”switch compares with strict equality (===) against each case. Execution falls through to the next case unless you break.
function describe(code) { switch (code) { case 200: case 201: case 204: return 'success'; // return also exits the switch case 301: case 302: return 'redirect'; default: return 'other'; }}Grouped cases like the above are intentional fallthrough. Accidental fallthrough is a classic bug:
switch (kind) { case 'a': console.log('a'); // missing break — 'b' also runs case 'b': console.log('b'); break;}A common trick is switch (true) for range logic:
switch (true) { case score >= 90: return 'A'; case score >= 80: return 'B'; default: return 'F';}for (let i = 0; i < 3; i++) { console.log(i); // => 0, 1, 2}All three clauses are optional: for (;;) is an infinite loop. Use let, not var, so each iteration gets its own binding (see syntax and types).
while and do..while
Section titled “while and do..while”while tests before the body; do..while tests after, so the body always runs at least once.
let n = 3;while (n > 0) { console.log(n); n--; }
let input;do { input = prompt('Enter a value'); // browser only} while (!input);for..of — values of an iterable
Section titled “for..of — values of an iterable”Works on arrays, strings, Map, Set, arguments, DOM node lists, and generators — anything implementing the iterable protocol.
for (const ch of 'héllo') process(ch); // iterates by code point
for (const [key, value] of Object.entries({ a: 1, b: 2 })) { console.log(key, value); // => a 1 / b 2}
const seen = new Set([1, 2, 2, 3]);for (const v of seen) console.log(v); // => 1, 2, 3Get the index with entries():
for (const [i, item] of ['a', 'b'].entries()) { console.log(i, item); // => 0 'a' / 1 'b'}for..in — enumerable string keys
Section titled “for..in — enumerable string keys”for..in iterates property names, including inherited ones, in an unspecified order for non-integer keys. It is for objects, and rarely the right tool even then.
const base = { inherited: true };const obj = Object.create(base);obj.own = 1;
for (const k in obj) console.log(k); // => 'own', 'inherited'Prefer Object.keys, Object.values, or Object.entries, which return own enumerable properties only:
for (const key of Object.keys(obj)) console.log(key); // => 'own'| Construct | Iterates | Works on |
|---|---|---|
for..of |
values | iterables (arrays, strings, Map, Set, …) |
for..in |
enumerable string keys, incl. inherited | any object |
Object.keys/values/entries |
own enumerable keys/values/pairs | plain objects |
.forEach |
values with index | arrays, Map, Set |
break, continue, and labels
Section titled “break, continue, and labels”break exits the nearest loop or switch. continue skips to the next iteration.
for (const n of [1, 2, 3, 4]) { if (n === 3) break; // stops entirely if (n % 2 === 0) continue; // skips evens console.log(n); // => 1}A label names a loop so break/continue can target an outer one — the clean way out of nested loops.
outer:for (const row of grid) { for (const cell of row) { if (cell === target) { found = cell; break outer; // exits both loops } }}Operator precedence, briefly
Section titled “Operator precedence, briefly”Precedence, highest to lowest for the operators you use daily:
() ?. [] . grouping, access, call** (right-associative)! - + ~ typeof unary* / %+ -<< >> >>>< <= > >= in instanceof== != === !==& ^ | bitwise&&|| ?? (?? cannot mix with && / || unparenthesized)? : ternary= += ||= &&= ??= assignment (right-associative), commaTwo things worth memorizing rather than looking up: ** binds tighter than unary minus (-2 ** 2 is a syntax error — write -(2 ** 2)), and && binds tighter than ||.
Key points
Section titled “Key points”+concatenates when either side is a string; every other arithmetic operator coerces to number.%is a remainder, not a modulo.- Use
===everywhere; the only defensible==isx == null.Object.isfixesNaNand-0. - Eight falsy values:
false,0,-0,0n,'',null,undefined,NaN. Everything else, including[]and{}, is truthy. &&and||return operands, not booleans, and short-circuit.??falls back only on null/undefined — prefer it over||for defaults.?.short-circuits one link at a time;a?.b.cstill throws ifbis missing.switchmatches with===and falls through withoutbreak; wrap case bodies in braces when they declare variables.for..offor values,Object.keys/entriesfor objects. Neverfor..inon an array.- Labels give clean exits from nested loops;
break/continuedo not work in.forEach.