Skip to content

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.

7 + 2; // => 9
7 - 2; // => 5
7 * 2; // => 14
7 / 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 7

Compound 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'; // => 10
true + 1; // => 2 (true coerces to 1)
1 < 2; // => true
2 <= 2; // => true
3 > 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.

=== (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'; // => false
1 == '1'; // => true
0 == false; // => true
'' == false; // => true
[] == false; // => true ← [] → '' → 0, false → 0
null == undefined; // => true
null == false; // => false ← null only equals undefined

When the types differ, == applies these steps in order:

  1. null == undefinedtrue. Neither is loosely equal to anything else.
  2. number == string → convert the string to a number.
  3. boolean == anything → convert the boolean to a number (true→1, false→0), then retry.
  4. object == primitive → convert the object to a primitive (via valueOf, then toString), then retry.
  5. bigint == number/string → compare mathematical values.

Walking through [] == false:

[] == false
[] == 0 (boolean → number)
'' == 0 (array → primitive: [].toString() is '')
0 == 0 (string → number)
true

And the famous non-transitive triple:

'0' == false; // => true ('0' → 0, false → 0)
'0' == 0; // => true
false == 0; // => true
'0' == ''; // => false (both strings, compared directly)

Object.is is === with two corrections: NaN equals itself, and +0 does not equal -0.

NaN === NaN; // => false
Object.is(NaN, NaN); // => true
Object.is(0, -0); // => false

=== on objects asks “is it the same object?”, never “does it have the same contents?”.

{ a: 1 } === { a: 1 }; // => false
const x = { a: 1 };
const y = x;
x === y; // => true

For 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).

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 NaN
if ([]) console.log('empty array is truthy'); // prints
if ({}) console.log('empty object is truthy'); // prints
if ('0') console.log('the string "0" is truthy'); // prints
if (0) console.log('never');

&& 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 execution
isReady && start();
// Fallback value
const 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.

?? returns the right side only when the left is null or undefined. Zero, empty string, and false pass through.

0 || 'default'; // => 'default' ← usually wrong
0 ?? 'default'; // => 0 ← usually right
'' ?? 'default'; // => ''
null ?? 'default'; // => 'default'
undefined ?? 'default'; // => 'default'

?. 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 undefined
user.settings?.theme; // => undefined

Three forms:

obj?.prop // property access
obj?.[key] // computed access
obj.method?.() // call only if the method exists
const config = null;
config?.get?.('key'); // => undefined, no error

It combines naturally with ??:

const theme = user.settings?.theme ?? 'light';

ES2021 added three operators that assign only when the short-circuit says to.

a ||= b; // a = a || b — assign if a is falsy
a &&= b; // a = a && b — assign if a is truthy
a ??= b; // a = a ?? b — assign if a is null or undefined

They 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 0
opts.label ||= 'n/a'; // becomes 'n/a'
opts.timeout ??= 500; // added: 500
// => { retries: 0, label: 'n/a', timeout: 500 }

These convert operands to 32-bit signed integers, operate bit by bit, and convert back to a double.

Operator Meaning Example
& AND 12 & 108
| OR 12 | 1014
^ XOR 12 ^ 106
~ NOT ~12-13
<< left shift 1 << 416
>> signed right shift -16 >> 2-4
>>> unsigned right shift -16 >>> 2815
// Flags
const READ = 1, WRITE = 2, EXEC = 4;
let perms = READ | WRITE; // => 3
(perms & WRITE) !== 0; // => true
perms &= ~WRITE; // remove WRITE => 1

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';
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 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 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);

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, 3

Get the index with entries():

for (const [i, item] of ['a', 'b'].entries()) {
console.log(i, item); // => 0 'a' / 1 'b'
}

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 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
}
}
}

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)
, comma

Two 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 ||.

  • + 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 == is x == null. Object.is fixes NaN and -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.c still throws if b is missing.
  • switch matches with === and falls through without break; wrap case bodies in braces when they declare variables.
  • for..of for values, Object.keys/entries for objects. Never for..in on an array.
  • Labels give clean exits from nested loops; break/continue do not work in .forEach.