Arithmetic, and the Odd Behaviour of +
An operator is a symbol that takes one or two values and produces a new one. You already know most of them from school arithmetic. What earns them a full lesson in JavaScript is that several behave in ways that surprise people, and those surprises are among the most common sources of quiet, hard-to-find bugs.
The arithmetic set is +, -, *, /, % and **. Two deserve extra attention. % is the remainder operator, not a percentage: 17 % 5 is 2, because 5 goes into 17 three times with 2 left over. It is how you test whether a number is even (n % 2 === 0), how you cycle repeatedly through a fixed list, and how you break a total number of seconds into minutes and seconds. ** is exponentiation, so 2 ** 10 is 1024.
Also note that / never does integer division. 17 / 5 is 3.4, not 3. If you want the whole part, use Math.floor() or Math.trunc() — a habit worth forming early if you are coming from C or Java, where the same expression gives 3.
+ is the genuine oddity, because it does two different jobs. Given two numbers it adds them. Given a string anywhere in the expression it switches to joining text, converting the other value to a string to do it. So 5 + 5 is 10 but '5' + 5 is the string '55'. Every other arithmetic operator goes the opposite way and converts strings into numbers, which is why '5' - 5 is 0. This inconsistency bites hardest when you read form fields, because the value of an input box is always a string, even on a number input.
// Arithmetic
console.log(17 + 5); // 22
console.log(17 - 5); // 12
console.log(17 * 5); // 85
console.log(17 / 5); // 3.4 - never integer division
console.log(17 % 5); // 2 - remainder
console.log(2 ** 10); // 1024 - exponentiation
// The + operator has a split personality
console.log(5 + 5); // 10
console.log('5' + 5); // '55' - a string wins, so this is text joining
console.log('5' - 5); // 0 - every other operator converts to number
// Which is exactly what goes wrong with a form field
const typed = document.getElementById('marks').value; // always a string
console.log(typed + 10); // '78' + 10 -> '7810'
console.log(Number(typed) + 10); // 88 - convert first, then add - Convert form input the moment you read it, not later:
const marks = Number(input.value). Doing the conversion at the boundary means every line after it can trust the type.
Assignment Shortcuts and Increment
= is assignment, not equality. It copies the value on the right into the name on the left, and it is a different thing entirely from == and ===, which ask a question. Confusing them inside a condition is a classic first-week bug: if (x = 5) assigns 5 and then treats 5 as the condition, so the branch always runs and no error is reported.
Every arithmetic operator has a compound form: +=, -=, *=, /=, %= and **=. total += 50 means exactly total = total + 50, just shorter and with one fewer place to mistype the variable name. ++ and -- add or subtract one, and you will meet them mostly as loop counters.
++ comes in two forms and the difference only matters when you use its result. count++ hands back the old value and then increases the variable; ++count increases first and hands back the new value. When the increment sits on a line by itself the two are identical, which is why the safe habit is to keep it on its own line and never bury it inside a larger expression.
ES2021 added three logical assignment operators that are worth knowing: ||=, &&= and ??=. Each assigns only if the current value passes the matching test, which makes "fill this in if it is missing" a single line.
let total = 100;
total += 50; // 150 same as total = total + 50
total -= 20; // 130
total *= 2; // 260
total /= 4; // 65
total %= 10; // 5
let count = 0;
count++; // 1
count--; // 0
// Postfix returns the OLD value, prefix returns the NEW one
let i = 5;
console.log(i++); // 5 (i is now 6)
console.log(++i); // 7
// Logical assignment (ES2021)
let nickname = '';
nickname ||= 'Guest'; // assigns, because '' is falsy -> 'Guest'
let discount = 0;
discount ??= 10; // does NOT assign - 0 is not null -> 0 ||=and??=look alike and are not the same.||=overwrites anything falsy, including0and''.??=overwrites onlynullandundefined. The section on??below explains why that difference matters.
== versus === and the Coercion Trap
JavaScript has two equality operators, and this is the one place where picking the wrong default causes real damage. === is strict equality: it checks the type first, and if the two types differ the answer is false straight away. == is loose equality: when the types differ it converts one side and then compares, following a table of rules that almost nobody has memorised.
Those conversions produce results that look like nonsense out of context. '5' == 5 is true because the string becomes a number. 0 == '' is true because both become 0. 0 == false is true for the same reason. null == undefined is true, but null == 0 is false, because null has a special rule of its own. None of this is random, but relying on it means anyone reading your code needs the whole conversion table in their head to be sure what a single line does.
So the rule for real projects is short: always use === and !==. The one common exception is value == null, a compact way of asking "is this null or undefined", and even that reads better written out in full. If you genuinely need to compare a string against a number, convert explicitly with Number() so the intention is visible on the line.
// Strict: the types must match
console.log(5 === 5); // true
console.log('5' === 5); // false
console.log(5 !== '5'); // true
// Loose: JavaScript converts first, then compares
console.log('5' == 5); // true
console.log(0 == ''); // true
console.log(0 == false); // true
console.log(null == undefined); // true
console.log(null == 0); // false - null has its own rule
// The bug this causes in real code
const marksEntered = '0'; // read from an input box
if (marksEntered == false) {
console.log('No marks entered'); // runs! '0' loosely equals false
}
if (marksEntered === '') {
console.log('Really empty'); // does not run - correct behaviour
} ===— compares type and value;falseat once if the types differ!==— the strict opposite==— converts before comparing; avoid it><>=<=— compare numbers, and compare strings character by character- Objects and arrays are compared by identity, not by contents
- Comparing two arrays or objects with
===asks whether they are the same object in memory, not whether they look alike.[1, 2] === [1, 2]isfalse, and always will be.
Logical Operators Return Values, Not Booleans
&& (and), || (or) and ! (not) combine conditions, and they appear inside almost every if you will write. What surprises people is that && and || do not hand back true or false. They hand back one of the two operands.
a || b evaluates a; if a is truthy it returns a and never even looks at b. Otherwise it returns b. a && b is the mirror image: if a is falsy it returns a, otherwise it returns b. Inside an if the result gets treated as a condition anyway, so the difference is invisible there — but it is precisely why const name = input || 'Guest' works as a default value, and why isLoggedIn && showDashboard() works as a guard.
Stopping early like this is called short-circuiting, and it is more than an optimisation. user && user.name is safe because when user is null the right-hand side is never evaluated at all. The practical habit that follows: put the cheap or protective check on the left, and the expensive or risky one on the right.
console.log(true && false); // false
console.log(true || false); // true
console.log(!true); // false
// They return an operand, not a boolean
console.log('Ananya' || 'Guest'); // 'Ananya'
console.log('' || 'Guest'); // 'Guest'
console.log('Ananya' && 'Guest'); // 'Guest'
console.log(null && 'Guest'); // null
// Short-circuiting used as a guard
const user = null;
console.log(user && user.name); // null - user.name never runs
// Combining conditions the usual way
const age = 19;
const hasIdProof = true;
if (age >= 18 && hasIdProof) {
console.log('Allowed');
} &&binds more tightly than||. When you mix the two, add brackets:(a || b) && canda || (b && c)mean different things, and the brackets cost nothing.
?? and ?. — Defaults That Do Not Eat Zero
Using || to supply a default has a well-known flaw. It replaces anything falsy, and 0 and '' are falsy. So const quantity = input || 1 silently turns a deliberate 0 into 1, and const note = typed || 'none' throws away an intentionally empty string. In a shopping cart or a marks sheet that is not a style issue, it is a wrong number on a screen.
The nullish coalescing operator ?? fixes it. It falls back only when the left side is null or undefined. Everything else — including 0, '' and false — passes through untouched. Whenever you are defaulting a value that could legitimately be zero or empty, such as a discount, a quantity or a score, ?? is the correct operator and || is a bug waiting to be reported.
Its companion is optional chaining ?., which stops a property lookup safely instead of throwing. user?.address?.city returns undefined when user or address is missing, rather than crashing with "cannot read properties of undefined". It works for method calls too, written obj.method?.(). Use it where a value is genuinely allowed to be absent. Sprinkling it over code where the value should always exist just hides real errors until later.
const settings = { discount: 0, label: '' };
console.log(settings.discount || 10); // 10 - wrong, the 0 was deliberate
console.log(settings.discount ?? 10); // 0 - correct
console.log(settings.label || 'None'); // 'None'
console.log(settings.label ?? 'None'); // '' - the empty string survives
// Optional chaining
const user = { name: 'Rahul' }; // no address property at all
// console.log(user.address.city); // TypeError
console.log(user.address?.city); // undefined - no crash
console.log(user.address?.city ?? 'City not set'); // 'City not set' ??cannot be mixed directly with&&or||in one expression — JavaScript throws aSyntaxErrorand asks for brackets. That restriction is deliberate, because the intended order would otherwise be ambiguous to a reader.
Truthy, Falsy and Precedence
Every value in JavaScript can be used as a condition. There are exactly eight falsy values and everything else is truthy, so memorising the short list is far easier than trying to learn the infinite other side.
The falsy values are false, 0, -0, 0n, '', null, undefined and NaN. Notice what is not on that list: '0', 'false', [] and {} are all truthy. An empty array being truthy surprises everyone at least once — to check whether an array has anything in it, test items.length > 0 rather than the array itself.
Finally, precedence. Operators are applied in a fixed order: ** first, then * / %, then + -, then the comparisons, then &&, then ||, and assignment last. You do not need the full table in memory. You need to know that it exists, and to add brackets the moment an expression is long enough that a reader might have to stop and work it out.
- Falsy:
false,0,-0,0n,'',null,undefined,NaN - Truthy: literally everything else, including
'0','false',[]and{} - Test an array for content with
items.length > 0, never withif (items) Boolean(value)converts explicitly;!!valuedoes the same in two characters- When mixing
&&,||and comparisons, use brackets instead of trusting precedence
- A thirty-second experiment settles this better than any explanation: open the console and type
Boolean([]), thenBoolean(''), thenBoolean('0'). Seeingtrue,false,truecome back makes the rule stick.
