JavaScript (ES2026) Module 2 – Values, types and operators
Operators, short-circuiting and precedence
How the arithmetic, assignment, comparison, logical and bitwise operators work, which group first, in which order operands run, and where to add parentheses.
What you will learn
- Use the arithmetic, assignment, comparison, logical and conditional operators correctly
- Predict how precedence and associativity group an expression, and in which order its operands run
- Combine and test bit flags with |, & and ~, and explain why the bitwise operators wrap at 32 bits
- Add parentheses wherever the grouping is not obvious to a reader
Before you start
What you’ll learn · Before you start
What you will learn
- Use the arithmetic, assignment, comparison, logical and conditional operators correctly
- Predict how precedence and associativity group an expression, and in which order its operands run
- Combine and test bit flags with |, & and ~, and explain why the bitwise operators wrap at 32 bits
- Add parentheses wherever the grouping is not obvious to a reader
Before you start
On this page
An operator combines values into a new value: + adds, < compares, && chooses. When an expression holds
several operators, three separate rules decide what happens. Precedence decides which operators group first:
* binds more tightly than +, so 2 + 3 * 4 is 14. Associativity decides how operators of the same
precedence group: most go from left to right, so 10 - 4 - 3 is 3, while ** and assignment go from right to left.
Evaluation order decides when each operand runs, and in JavaScript that is always from left to right, whatever the
grouping, except that &&, ||, ?? and ? : may skip an operand altogether. This lesson walks through each
family of operators, shows the precedence traps that cause real bugs, and ends with bit flags, the main everyday use
of the bitwise operators.
Arithmetic: + - * / % and **
// The arithmetic operators on a few awkward inputs.
const seven = 7;
const two = 2;
console.log(seven / two, Math.trunc(seven / two)); // no integer division: truncate it yourself
console.log(-seven % 3, seven % -3, 5.5 % two); // % keeps the sign of the left operand, and works on fractions
console.log(two ** 3 ** two, (two ** 3) ** two); // ** groups from the right
console.log(1 / 0, -1 / 0, 0 / 0);
// A true modulo, for wrapping round: three days before Tuesday (day 2) is Saturday (day 6).
const daysInWeek = 7;
const tuesday = 2;
console.log((tuesday - 3) % daysInWeek, (((tuesday - 3) % daysInWeek) + daysInWeek) % daysInWeek); Output
3.5 3 -1 1 1.5 512 64 Infinity -Infinity NaN -1 6
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node arithmetic.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
/gives the exact quotient, fraction included.7 / 2is 3.5; cut off the fraction withMath.truncwhen you want integer division.%is a remainder, not a modulo. Its result takes the sign of the left operand, so-7 % 3is -1 and7 % -3is 1, and it works on fractions too:5.5 % 2is 1.5. To wrap round a cycle, such as the days of a week, use((n % m) + m) % m: counting Sunday as day 0, three days before Tuesday is then Saturday, day 6, instead of day -1.**raises to a power and groups from the right, as in mathematics:2 ** 3 ** 2is2 ** 9, 512, while(2 ** 3) ** 2is 64.- Dividing by zero does not throw.
1 / 0isInfinity,-1 / 0is-Infinityand0 / 0isNaN.
+ also joins strings, by the rules of Type conversion and coercion, so it is
the one arithmetic operator whose meaning depends on its operands. Two groupings are refused outright:
// Two groupings the grammar refuses, so the reader never has to guess. Each source is compiled on its own here.
const sources = ['-2 ** 2', '(-2) ** 2', '-(2 ** 2)', 'a ?? b || c', '(a ?? b) || c'];
for (const source of sources) {
try {
// eslint-disable-next-line no-new-func -- compiling each snippet is the point of this example
const run = new Function('a', 'b', 'c', `return ${source};`);
console.log(`${source.padEnd(14)} gives ${run(null, 0, 5)}`);
} catch (error) {
console.log(`${source.padEnd(14)} ${error.name}`);
}
} Output
-2 ** 2 SyntaxError (-2) ** 2 gives 4 -(2 ** 2) gives -4 a ?? b || c SyntaxError (a ?? b) || c gives 5
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node syntax-errors.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
-2 ** 2 could mean (-2) ** 2, which is 4, or -(2 ** 2), which is -4, and readers who know other languages
expect different answers, so JavaScript refuses it with a SyntaxError and you write the parentheses you mean. For
the same reason, ?? cannot sit next to || or && without parentheses.
Assignment, increment and decrement
// Compound assignment: x op= y means x = x op y.
let balance = 100;
balance -= 30;
balance *= 2;
balance **= 1;
console.log(balance);
// Assignment is an expression whose value is the value assigned, and it groups from the right.
let low;
let high;
low = high = 0;
low -= 1;
high += 1;
console.log(low, high);
// x++ gives the old value, ++x the new one.
let count = 5;
console.log(count++, count, ++count, count);
// Inside a larger expression they are hard to follow: what does this array hold?
let i = 0;
const picked = [i++, i++, ++i];
console.log(picked, i); Output
140 -1 1 5 6 7 7 [ 0, 1, 3 ] 3
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node assignment.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
- Compound assignment writes
x op= yforx = x op y, for every arithmetic and bitwise operator:-=,*=,**=,|=and the rest, plus the logical forms&&=,||=and??=. - Assignment is an expression: its value is the value assigned, and it groups from the right, so
low = high = 0setshighfirst and thenlow. The same fact explains a classic bug,if (status = 'done'), which assigns instead of comparing and is always true; ESLint’sno-cond-assignrule reports it. x++gives the old value and++xthe new one, and both changex.[i++, i++, ++i]holds 0, 1 and 3, which few readers work out at a glance. Give increments a line of their own, or writei += 1.
Comparing and choosing
The comparison operators always give a boolean, and the earlier lessons of this module cover their rules:
Equality: ==, === and Object.is for === and ==, and
Type conversion and coercion for < and >. Two more facts belong here.
- Comparisons do not chain.
big > middle > smallreads like mathematics, but it groups as(big > middle) > small: the first comparison givestrue, which converts to 1, and1 > 1isfalse. Writebig > middle && middle > small. &&,||and??return one of their operands and may skip the right one, as Booleans, truthy and falsy values showed. The conditional operatorcondition ? a : bevaluates only one of its branches, and it groups from the right, soa ? b : c ? d : emeansa ? b : (c ? d : e). One level of it reads well; for more, useifor a lookup table.
Precedence and associativity
The ECMAScript standard has no precedence table: its grammar defines one expression inside another, and the nesting decides the grouping. Read from the top, this is the order that results, the most tightly binding first:
| Group | Operators | Same-level operators group |
|---|---|---|
| 1 | parentheses ( … ) |
|
| 2 | a.b, a[b], a?.b, calls f(), new X() |
from the left |
| 3 | postfix x++, x-- |
|
| 4 | prefix !, ~, +x, -x, ++x, --x, typeof, void, delete, await |
|
| 5 | ** |
from the right |
| 6 | *, /, % |
from the left |
| 7 | +, - |
from the left |
| 8 | <<, >>, >>> |
from the left |
| 9 | <, <=, >, >=, in, instanceof |
from the left |
| 10 | ==, !=, ===, !== |
from the left |
| 11 | & |
from the left |
| 12 | ^ |
from the left |
| 13 | | |
from the left |
| 14 | && |
from the left |
| 15 | || and ??, which cannot be mixed without parentheses |
from the left |
| 16 | ? : |
from the right |
| 17 | = and every compound assignment, =>, yield |
from the right |
| 18 | the comma , |
from the left |
You do not need to memorise the table. You need to recognise the five places where it surprises people, and add parentheses there:
// Five expressions that group differently from how they read, each with the parentheses it needed.
const SHARE = 4;
const withShare = 5; // bits 4 and 1
const withoutBit1 = 6; // bits 4 and 2
const a = 2;
const b = 3;
const big = 3;
const middle = 2;
const small = 1;
const user = null;
// eslint-disable-next-line no-self-compare -- the bug: === groups first and compares SHARE with itself
console.log('1.', withShare & SHARE === SHARE, withoutBit1 & SHARE === SHARE);
console.log(' ', (withShare & SHARE) === SHARE, (withoutBit1 & SHARE) === SHARE);
console.log('2.', 'Total: ' + a + b);
console.log(' ', 'Total: ' + (a + b));
console.log('3.', typeof a + 1);
console.log(' ', typeof (a + 1));
console.log('4.', big > middle > small);
console.log(' ', big > middle && middle > small);
// eslint-disable-next-line no-unsafe-negation -- the mistake this line shows
console.log('5.', !user instanceof Object);
console.log(' ', !(user instanceof Object)); Output
1. 1 0
true true
2. Total: 23
Total: 5
3. number1
number
4. false
true
5. false
true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node precedence-traps.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
- The bitwise operators bind less tightly than the comparisons.
withShare & SHARE === SHAREgroups aswithShare & (SHARE === SHARE), which iswithShare & true, which iswithShare & 1. It gives 1 for 5 (looks right, by accident) and 0 for 6, although 6 has the SHARE bit. ESLint’sno-self-comparerule, when it is switched on, reports the hiddenSHARE === SHARE; the comment above that line in the example silences it on purpose. +joins from the left.'Total: ' + a + bjoins'Total: 2'and then3. Group the sum, or write a template literal,`Total: ${a + b}`.typeofapplies to the operand right after it.typeof a + 1is'number' + 1, which is'number1'.- Comparisons do not chain, as the previous section showed.
!binds more tightly thaninstanceofandin.!user instanceof Objectisfalse instanceof Objectortrue instanceof Object, which is alwaysfalse. Write!(user instanceof Object); ESLint’sno-unsafe-negationrule, in its recommended set, reports the unparenthesised form.
How perms & SHARE === SHARE is grouped, and how it was meant
Text description of the diagram
Two small expression trees, one above the other. In each tree, an operator sits above the operands it applies to.
- Without parentheses, perms & SHARE === SHARE is grouped as perms & (SHARE === SHARE). The top of the tree is &. Its operands are perms and a === node, which compares SHARE with SHARE. That comparison is always true, so the whole expression becomes perms & true, which is perms & 1.
- With parentheses, (perms & SHARE) === SHARE has === at the top. Its operands are an & node, which keeps the SHARE bit of perms, and SHARE itself. This is the test the author meant: it is true exactly when the SHARE bit is on.
Grouping is not evaluation order
Precedence decides which operator applies to which operands. It does not decide what runs first: JavaScript evaluates operands from left to right, then applies the operators as the grouping says.
// Grouping decides which operator applies to what; the operands still run from left to right.
const calls = [];
function value(n) {
calls.push(n);
return n;
}
console.log(value(1) + value(2) * value(3), calls.join(' then '));
calls.length = 0;
console.log(value(2) ** value(3) ** value(2), calls.join(' then '));
// The skipping operators are the exception: an operand that cannot change the result never runs.
calls.length = 0;
const result = value(0) && value(4);
const chosen = value(5) > 1 ? value(6) : value(7);
console.log(result, chosen, calls.join(' then ')); Output
7 1 then 2 then 3 512 2 then 3 then 2 0 6 0 then 5 then 6
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node evaluation-order.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
value(1) + value(2) * value(3) multiplies before it adds, yet the calls happen in the order 1, 2, 3. ** groups from
the right, and its operands still run from the left: 2, then 3, then 2. The last line shows the exception: && skips
its right operand when the left one is falsy, and ? : runs only one branch, so value(4) and value(7) never run.
Code that depends on the order of side effects inside one expression is hard to read even when it is correct, so
give each side effect a statement of its own.
Bitwise operators and flags
&, |, ^, ~, <<, >> and >>> work on 32-bit integers. Their operands are converted to 32-bit integers
(by the conversions the standard calls ToInt32 and, for the unsigned shift >>>, ToUint32), the bits are combined,
and the result is a number again:
// The bitwise operators turn their operands into 32-bit integers first.
const values = [5.7, -5.7, 2 ** 31 - 1, 2 ** 31, 3_000_000_000];
for (const value of values) {
console.log(String(value).padEnd(12), 'x | 0 =', String(value | 0).padEnd(12), 'Math.trunc =', Math.trunc(value));
}
// >>> 0 reads the same 32 bits as an unsigned number, ~ flips every bit, << shifts left.
const minusOne = -1;
console.log(minusOne >>> 0, ~5, 1 << 31, 1 << 32); Output
5.7 x | 0 = 5 Math.trunc = 5 -5.7 x | 0 = -5 Math.trunc = -5 2147483647 x | 0 = 2147483647 Math.trunc = 2147483647 2147483648 x | 0 = -2147483648 Math.trunc = 2147483648 3000000000 x | 0 = -1294967296 Math.trunc = 3000000000 4294967295 -6 -2147483648 1
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node bitwise-32.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
5.7 | 0 drops the fraction, which is why x | 0 and ~~x appear in old code as a fast way to truncate. But
numbers beyond 32 bits wrap round: 2 ** 31 | 0 is -2,147,483,648 and 3_000_000_000 | 0 is -1,294,967,296. Use
Math.trunc to drop a fraction; it works for every number.
Their everyday job is holding flags: a group of on-or-off options stored together in a single number, with one bit for each option.
// Four permissions, one bit each.
const READ = 1; // 0b0001
const WRITE = 2; // 0b0010
const SHARE = 4; // 0b0100
const DELETE = 8; // 0b1000
const bits = (n) => n.toString(2).padStart(4, '0');
let editor = READ | WRITE; // combine with |
console.log('editor ', bits(editor), editor);
editor |= SHARE; // add a flag
console.log('add SHARE ', bits(editor), editor);
editor &= ~WRITE; // remove a flag: & with every other bit
console.log('remove WRITE ', bits(editor), editor);
console.log('can share? ', (editor & SHARE) !== 0);
console.log('can delete? ', (editor & DELETE) !== 0);
const needed = READ | SHARE;
console.log('has READ and SHARE?', (editor & needed) === needed); Output
editor 0011 3 add SHARE 0111 7 remove WRITE 0101 5 can share? true can delete? false has READ and SHARE? true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node flags.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
| To | Write | Why it works |
|---|---|---|
| combine flags | READ | WRITE |
| switches on every bit that is on in either operand |
| add a flag | perms |= SHARE |
switching on a bit that is already on changes nothing |
| remove a flag | perms &= ~WRITE |
~WRITE has every bit on except WRITE’s |
| test one flag | (perms & SHARE) !== 0 |
& keeps only the bits on in both |
| test several flags | (perms & needed) === needed |
every bit of needed must survive the & |
| flip a flag | perms ^= SHARE |
^ switches a bit on if it was off, and off if it was on |
Flags keep a set of options small and fast to test, which is why browsers report the mouse buttons held down
during a mouse event this way, in its buttons property: 1 for the primary button, 2 for the secondary one, 4 for
the middle one, added together. For a handful of settings in application code, an object with named booleans is
easier to read; reach for flags when you store or send many of them.
The comma operator and other rarities
- The comma operator evaluates both operands and gives the right one:
(1, 2, 3)is 3. Its one common home is the last part of aforloop, as ini++, j--; elsewhere it hides what code does, and linters warn about it. The commas between function arguments, array items and declarations are separators, not this operator. void expressionevaluates the expression and givesundefined. You will seevoid 0in old or generated code as a way to writeundefined.typeof,delete,inandinstanceofare operators too, with the precedence shown above. Their meaning belongs to the lessons on types, objects and classes.
Exercise: permission flags
Exercise · Easy · JavaScript
Grant, revoke and test permission flags
A document app stores each user's permissions as one number, with one bit per permission:
- READ is 1, bits
0001; - WRITE is 2, bits
0010; - SHARE is 4, bits
0100; - DELETE is 8, bits
1000.
Write four functions, each a single expression with the bitwise operators:
grant(perms, flags)returnspermswith every bit offlagsswitched on;revoke(perms, flags)returnspermswith every bit offlagsswitched off, and leaves bits that were already off alone;hasAll(perms, flags)returnstruewhen every bit offlagsis on inperms(andtruewhenflagsis 0);hasAny(perms, flags)returnstruewhen at least one bit offlagsis on inperms.
grant(1, 4) // 5: READ, then SHARE added
revoke(7, 2) // 5: WRITE taken away from READ | WRITE | SHARE
hasAll(6, 4) // true: 6 is WRITE | SHARE
hasAny(1, 2 | 4) // false
The starter code uses +, - and a comparison without parentheses. Run the sample tests to see where each one goes wrong.
Starter code · flags.mjs
// READ = 1, WRITE = 2, SHARE = 4, DELETE = 8: one bit per permission.
/** perms with every bit of flags switched on. */
export function grant(perms, flags) {
return perms + flags;
}
/** perms with every bit of flags switched off. */
export function revoke(perms, flags) {
return perms - flags;
}
/** True when every bit of flags is on in perms. */
export function hasAll(perms, flags) {
// eslint-disable-next-line no-self-compare -- the starter's mistake: === groups before &
return Boolean(perms & flags === flags);
}
/** True when at least one bit of flags is on in perms. */
export function hasAny(perms, flags) {
return perms > 0 && flags > 0;
} The sample tests · flags.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { grant, revoke, hasAll, hasAny } from './flags.mjs';
const READ = 1;
const WRITE = 2;
const SHARE = 4;
const DELETE = 8;
test('grant switches bits on', () => {
assert.equal(grant(READ, SHARE), 5);
assert.equal(grant(READ | WRITE, SHARE | DELETE), 15);
assert.equal(grant(0, WRITE), 2);
});
test('granting a flag that is already on changes nothing', () => {
assert.equal(grant(READ, READ), 1);
assert.equal(grant(READ | WRITE, WRITE | SHARE), 7);
});
test('revoke switches bits off', () => {
assert.equal(revoke(READ | WRITE | SHARE, WRITE), 5);
assert.equal(revoke(15, READ | DELETE), 6);
});
test('revoking a flag that is already off changes nothing', () => {
assert.equal(revoke(READ, WRITE), 1);
assert.equal(revoke(READ | SHARE, WRITE | SHARE), 1);
});
test('hasAll needs every bit of flags', () => {
assert.equal(hasAll(READ | WRITE, READ), true);
assert.equal(hasAll(READ | WRITE, READ | SHARE), false);
assert.equal(hasAll(WRITE | SHARE, SHARE), true);
assert.equal(hasAll(READ | SHARE, SHARE), true);
assert.equal(hasAll(READ, SHARE), false);
assert.equal(hasAll(READ, 0), true);
});
test('hasAny needs at least one bit of flags', () => {
assert.equal(hasAny(READ | WRITE, WRITE | SHARE), true);
assert.equal(hasAny(READ, WRITE | SHARE), false);
assert.equal(hasAny(DELETE, DELETE), true);
assert.equal(hasAny(READ | WRITE, 0), false);
});
test('a short history of one user', () => {
let perms = 0;
perms = grant(perms, READ | SHARE);
perms = revoke(perms, SHARE);
perms = grant(perms, WRITE);
assert.equal(perms, 3);
assert.equal(hasAll(perms, READ | WRITE), true);
assert.equal(hasAny(perms, SHARE | DELETE), false);
}); A hint
| switches bits on, and switching on a bit that is already on changes nothing, which + cannot promise. & ~flags keeps every bit except those of flags. For the tests, put parentheses around the &: === and !== bind more tightly than &, so perms & flags === flags compares flags with itself first.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Self-check
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
What is the difference between operator precedence and evaluation order in JavaScript? Precedence and
associativity decide how an expression is grouped: in a + b * c the multiplication groups first, and in
a - b - c the left subtraction does. Evaluation order decides when each operand runs, and in JavaScript operands
always run from left to right, whatever the grouping: in f() + g() * h(), f is called first, then g, then h,
and only then are the multiplication and the addition done. The exceptions are the operators that can skip an
operand, &&, ||, ?? and ? :. Parentheses change the grouping, never the left-to-right order of the operands.
That is why two side effects in one expression, such as i++ + i++, are legal but hard to read.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
An admin page is guarded by if (user.perms & ADMIN === ADMIN), with ADMIN = 8. Users with odd permission values, such as 5, get in, while some real admins, such as 12, are refused. Explain exactly why, fix the check, and say how you would stop this class of bug.
-
Fresher to mid
A billing service turns amounts into whole cents with Math.round(amount * 100) | 0. It ran for years, until one invoice for $25,000,000.00 was stored as a negative number. Work out the exact value it produced, explain why, and say what you would change.
Key takeaways
- Precedence groups operators, associativity groups operators of equal precedence, and operands always run from
left to right, except where
&&,||,??and? :skip one. /keeps the fraction (useMath.truncfor integer division),%keeps the sign of the left operand, and**groups from the right;-2 ** 2is aSyntaxError.- Assignment is an expression that groups from the right;
x++gives the old value; keep increments out of larger expressions. - Comparisons do not chain:
a > b > cmeans(a > b) > c. - The bitwise operators bind less tightly than
===, so write(perms & FLAG) === FLAG; they also convert to 32 bits, so useMath.truncrather than| 0to drop a fraction. - Flags combine with
|, are removed with& ~, tested with&and flipped with^. - When the grouping is not obvious to a reader, add parentheses.
References
- ECMAScript 2026 Language Specification: ECMAScript Language: Expressions (Ecma International)
- ECMAScript 2026 Language Specification: Exponentiation Operator (Ecma International)
- ECMAScript 2026 Language Specification: Multiplicative Operators and the Number remainder (Ecma International)
- ECMAScript 2026 Language Specification: Binary Bitwise Operators (Ecma International)
- ECMAScript 2026 Language Specification: ToInt32 (Ecma International)
- ECMAScript 2026 Language Specification: Binary Logical Operators (Ecma International)
- ECMAScript 2026 Language Specification: Comma Operator (Ecma International)
- MouseEvent.buttons (MDN Web Docs)
- Operator precedence (MDN Web Docs)
- no-unsafe-negation: disallow negating the left operand of relational operators (ESLint)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress