JavaScript Operators
Master JavaScript operators: arithmetic, strict vs loose equality, logical AND/OR/NOT, short-circuit evaluation, nullish coalescing (??), and optional chaining (?.)
Every developer reaches a moment where some clever JavaScript one-liner just makes sense without any explanation. Something like user.score ?? 0 or isLoggedIn && renderDashboard(). If you’re not there yet, this lesson is how you get there.
Operators are the symbols and keywords that actually do the work in JavaScript. Every arithmetic calculation, every conditional check, every “is this value missing?” question runs through an operator. Understanding them, really understanding them, changes how you read and write code.
In this lesson, we’ll go through all the arithmetic operators, compare the two equality operators === and == (and why the difference genuinely matters), cover logical AND, OR, and NOT, and then look at two operators that modern JavaScript developers rely on constantly: nullish coalescing ?? and optional chaining ?.. We’ll finish with operator precedence, the rules that decide the order JavaScript evaluates your expressions.
We covered type coercion in the last lesson, and some of it will come up here when we talk about ==. We also looked at booleans, null, and undefined earlier, and that’ll be useful for the logical operators section. If any of those concepts feel rusty, a quick review before continuing won’t hurt.
Let’s get into it.
01What Are Operators in JavaScript?
An operator is a symbol (or keyword) that performs an action on one or more values. The values it works on are called operands.
In 5 + 3, the + is the operator, and 5 and 3 are the operands. That’s as simple as it gets. But operators do far more than math: they compare values, combine conditions, handle missing data, and control the logic of your entire program.
They fall into a few categories: arithmetic, assignment, comparison, logical, and the newer nullish and optional chaining operators. We’re covering all of them here.
02Arithmetic Operators in JavaScript: The Complete Toolkit
Let’s start with the ones you probably know, then get into the ones that trip people up.
The Basic Four: Addition, Subtraction, Multiplication, Division
These work exactly as you’d expect:
let a = 10;
let b = 3;
console.log(a + b); // 13
console.log(a - b); // 7
console.log(a * b); // 30
console.log(a / b); // 3.3333333333333335
Division always returns a float if it doesn’t divide evenly. 10 / 3 gives you 3.333..., not 3. If you need an integer, use Math.floor() or Math.trunc(). We covered floating point behavior in depth in the numbers article.
One important thing: the + operator does double duty. It adds numbers and concatenates strings. When a string is involved, everything gets converted to a string:
console.log(5 + 3); // 8
console.log("Hello " + "JS"); // "Hello JS"
console.log("Score: " + 100); // "Score: 100"
console.log(1 + 2 + "3"); // "33" (1+2 = 3, then 3+"3" = "33")
That last one surprises people. JavaScript evaluates left to right, so 1 + 2 runs first (giving 3), and then 3 + "3" triggers string concatenation.
The Remainder (Modulo) Operator: % Is More Useful Than You Think
The % operator returns the remainder after division:
console.log(10 % 3); // 1 → 10 ÷ 3 = 3 remainder 1
console.log(10 % 5); // 0 → 10 ÷ 5 = 2 remainder 0
console.log(10 % 7); // 3 → 10 ÷ 7 = 1 remainder 3
The most common use case for the JavaScript modulo operator: checking if a number is even or odd.
function isEven(n) {
return n % 2 === 0;
}
console.log(isEven(4)); // true
console.log(isEven(7)); // false
console.log(isEven(0)); // true
It also shows up when you need to “wrap around” through a list: cycling back to index 0 after you’ve reached the end. You’ll encounter that pattern once you start working with arrays and loops more seriously.
Exponentiation: The ** Operator
The ** operator raises the left value to the power of the right value. It came in with ES2016 and is fully supported everywhere today:
console.log(2 ** 3); // 8 → 2 to the power of 3
console.log(5 ** 2); // 25 → 5 squared
console.log(9 ** 0.5); // 3 → square root of 9 (same as Math.sqrt(9))
console.log(2 ** 10); // 1024
Before **, you’d write Math.pow(2, 3). Both work. But ** reads more naturally in expressions, especially when you’re chaining math operations.
Increment and Decrement Operators in JavaScript: ++ and —
These two add or subtract 1 from a value. You’ll use them constantly, especially in loops:
let count = 5;
count++; // same as: count = count + 1
console.log(count); // 6
count--; // same as: count = count - 1
console.log(count); // 5
Here’s the part that trips people up: the position of ++ matters when you use it inside an expression.
count++(postfix): returns the current value first, then increments++count(prefix): increments first, then returns the new value
let x = 5;
console.log(x++); // 5 → returns 5 first, then x becomes 6
console.log(x); // 6
let y = 5;
console.log(++y); // 6 → increments to 6 first, then returns 6
console.log(y); // 6
In practice, if you’re just incrementing on its own line (i++ in a for loop, for example), the position doesn’t matter because you’re not using the return value. The difference only shows up when you use it inline inside a larger expression.
Arithmetic Operator Explorer
13
03Assignment Operators: Shorthand That Keeps Code Clean
The basic assignment operator is =. It puts the value on the right into the variable on the left:
let score = 0;
score = 10; // now score is 10
JavaScript also has compound assignment operators that combine an operation with assignment in one step. These are shorthand, and they show up constantly in real code:
let score = 10;
score += 5; // score = score + 5 → 15
score -= 3; // score = score - 3 → 12
score *= 2; // score = score * 2 → 24
score /= 4; // score = score / 4 → 6
score %= 4; // score = score % 4 → 2
score **= 3; // score = score ** 3 → 8
console.log(score); // 8
There are also logical assignment operators that we’ll touch on after the next few sections, once the logical and nullish coalescing operators make sense. They follow the same shorthand pattern: ||=, &&=, and ??= each assign a value only under specific conditions.
04Comparison Operators: Asking Questions About Values
Comparison operators compare two values and return a boolean: true or false. They’re the foundation of every if-statement and loop condition you’ll ever write.
let a = 10;
let b = 5;
console.log(a > b); // true → greater than
console.log(a < b); // false → less than
console.log(a >= 10); // true → greater than or equal to
console.log(b <= 5); // true → less than or equal to
console.log(a == 10); // true → loose equality
console.log(a === 10); // true → strict equality
console.log(a != b); // true → loose inequality
console.log(a !== b); // true → strict inequality
The >, <, >=, <= operators work intuitively with numbers. With strings, they compare alphabetically based on Unicode values. With mixed types (say, a string compared to a number), JavaScript does type coercion, which can produce unexpected results. That's a good reason to make sure you're comparing values of the same type whenever possible.
Now let's look closely at the two equality operators, because this is where most early bugs hide.
05Strict Equality (===) vs Loose Equality (==): Why This Distinction Matters
This is one of JavaScript's most important topics for beginners to get right. Not because it's complicated, but because getting it wrong causes bugs that are hard to track down.
How == Works (Loose Equality)
The == operator checks if two values are equal, but it allows type coercion. If the types are different, JavaScript tries to convert one (or both) before comparing:
console.log(5 == "5"); // true → "5" gets converted to 5
console.log(0 == false); // true → false gets converted to 0
console.log("" == false); // true → both convert to 0
console.log(null == undefined); // true → special rule, these are equal to each other
console.log(null == 0); // false → null only equals null and undefined
The rules for how == decides what to convert are part of JavaScript's Abstract Equality Comparison algorithm. We went through it in detail in the type coercion lesson. The short version: the conversions are sometimes surprising, and that unpredictability is the problem.
How === Works (Strict Equality)
The === operator does not allow type coercion. If the types differ, the answer is false immediately, no conversion attempted:
console.log(5 === "5"); // false → different types, full stop
console.log(0 === false); // false → number vs boolean
console.log("" === false); // false → string vs boolean
console.log(null === undefined); // false → different types
console.log(5 === 5); // true → same type AND same value
console.log("hello" === "hello"); // true
Strict equality is predictable: same type and same value gives true. Different types always gives false. That's the whole algorithm.
Why === Is Almost Always the Right Choice
The rule most experienced developers follow: use === and !== by default. The only common exception is the intentional null check:
// This catches both null and undefined in one check (intentional use of ==)
if (value == null) {
console.log("value is null or undefined");
}
// The equivalent with ===, which requires two checks:
if (value === null || value === undefined) {
console.log("value is null or undefined");
}
Because null == undefined is true in JavaScript, some developers use value == null as a deliberate shorthand. Outside of this specific pattern, stick with ===.
And one special case: NaN is the only value in JavaScript that doesn't equal itself, with either operator:
console.log(NaN === NaN); // false ← always false, no exceptions
console.log(NaN == NaN); // false ← same
// The right way to check for NaN:
console.log(Number.isNaN(NaN)); // true
console.log(Number.isNaN("hello")); // false (it's a string, not NaN)
Strict === vs Loose == Comparison: Common Gotchas
| Expression | == (loose) | === (strict) |
|---|---|---|
| 5 == "5" | true | false |
| 0 == false | true | false |
| "" == false | true | false |
| null == undefined | true | false |
| null == 0 | false | false |
| [] == false | true | false |
| 5 === 5 | true | true |
| "hi" === "hi" | true | true |
| NaN === NaN | false | false |
Every row where == and === give different results is a potential bug waiting to happen. This is exactly why strict equality is the default choice.
06Logical Operators in JavaScript: AND, OR, and NOT
Logical operators let you combine boolean conditions. They're the core of decision-making in JavaScript: "do this if both conditions are met," "do this if either condition is met," "do this if a condition is NOT met."
The AND Operator (&&)
Returns true only if both operands are truthy:
let age = 25;
let hasID = true;
console.log(age >= 18 && hasID); // true → both conditions pass
console.log(age >= 18 && !hasID); // false → second condition fails
console.log(age < 18 && hasID); // false → first condition fails
The OR Operator (||)
Returns true if at least one operand is truthy:
let isAdmin = false;
let isModerator = true;
console.log(isAdmin || isModerator); // true → one of them is true
console.log(isAdmin || !isModerator); // false → both are false
The NOT Operator (!)
Flips a boolean: true becomes false, false becomes true. It also converts non-booleans first based on their truthiness:
let isLoggedIn = false;
console.log(!isLoggedIn); // true → flips false to true
console.log(!true); // false
console.log(!0); // true → 0 is falsy
console.log(!"hello"); // false → "hello" is truthy
The double NOT !! is a common pattern for converting any value to its boolean equivalent:
console.log(!!0); // false
console.log(!!"hello"); // true
console.log(!!null); // false
console.log(!!1); // true
If you want to review which values are truthy and which are falsy, we covered the full list in the booleans and falsy fundamentals article.
Short-Circuit Evaluation in JavaScript
Here's something that surprises most beginners: && and || don't always return true or false. They return one of their actual operand values. This behavior is called short-circuit evaluation, and it powers some very common JavaScript patterns.
For &&: evaluates left to right. If the left side is falsy, it returns that value immediately (short-circuits). If left is truthy, it returns whatever is on the right:
console.log(0 && "hello"); // 0 → left is falsy, returns left
console.log("hi" && "hello"); // "hello" → left is truthy, returns right
console.log(null && true); // null → left is falsy, returns left
console.log(1 && 2 && 3); // 3 → all truthy, returns last value
For ||: if the left side is truthy, it returns it immediately. If left is falsy, it returns the right side:
console.log("hello" || "world"); // "hello" → left is truthy, returns left
console.log(0 || "default"); // "default" → left is falsy, returns right
console.log(null || undefined); // undefined → both falsy, returns right
console.log(false || 0 || "yes"); // "yes" → first truthy value in the chain
This is why || has traditionally been used for default values:
function greet(name) {
name = name || "Guest";
return `Hello, ${name}!`;
}
console.log(greet("Alice")); // "Hello, Alice!"
console.log(greet("")); // "Hello, Guest!" → empty string is falsy
console.log(greet()); // "Hello, Guest!" → undefined is falsy
This pattern works, but it has a problem: it treats 0, "", and false as "no value," even when they're intentional. If someone passes 0 as a score, it gets silently replaced with the default. That's what ?? solves, and we'll get to it right after this.
&& short-circuit is also used to conditionally run code. You'll see this pattern a lot in React:
let user = { name: "Alice", isAdmin: true };
// Only runs console.log if isAdmin is truthy
user.isAdmin && console.log("Welcome, admin!");
// In React JSX, it looks like: {isAdmin && }
Logical Operators: Interactive Truth Table
07The Nullish Coalescing Operator (??) in JavaScript
The ?? operator was added in ES2020, and it exists specifically to fix the problem with using || for default values.
The issue, again, is that || treats all falsy values as "no value." But 0, "", and false are legitimate values in many situations. You don't want a volume setting of 0 silently replaced with your default of 50.
The nullish coalescing operator only falls back to the right side if the left side is null or undefined. Nothing else. Not 0. Not "". Not false.
let volume = 0;
console.log(volume || 50); // 50 ← wrong! 0 is falsy, gets replaced
console.log(volume ?? 50); // 0 ← correct. 0 is a real value, kept as-is
let username = "";
console.log(username || "Anonymous"); // "Anonymous" ← empty string is falsy
console.log(username ?? "Anonymous"); // "" ← empty string is not null/undefined
let data = null;
console.log(data ?? "No data"); // "No data" → null triggers the fallback
let notSet;
console.log(notSet ?? "Default"); // "Default" → undefined triggers the fallback
When to Use ?? Over ||
The mental model is simple:
- Use
||when you want a fallback for any falsy value (including0,"",false) - Use
??when you only want a fallback for missing values (nullorundefined)
In most real-world scenarios where you're setting defaults from API responses or user inputs, ?? is the safer choice. The ??= assignment operator follows the same logic:
let settings = { theme: null };
settings.theme ??= "dark"; // assigns "dark" because theme is null
console.log(settings.theme); // "dark"
settings.theme ??= "light"; // no change: "dark" is not null/undefined
console.log(settings.theme); // "dark"
?? vs || : See the Difference in Action
Fallback value is "DEFAULT". Pick a test value:
08Optional Chaining (?.) in JavaScript: Safe Access to Deep Properties
This operator saves you from one of the most common JavaScript runtime errors: "Cannot read properties of undefined."
Here's the scenario. You have a nested object from an API, and some of the nested properties might not exist. If you try to access a property on null or undefined, JavaScript throws:
let user = {
name: "Alice",
address: null // no address on file
};
// This line throws: "Cannot read properties of null (reading 'city')"
console.log(user.address.city);
Before optional chaining, the fix required verbose guard checks:
// The old way:
let city = user.address && user.address.city ? user.address.city : undefined;
// Or with a chain of ternaries and &&... not fun to write or read
Optional chaining makes this clean and readable:
let user = {
name: "Alice",
address: null
};
console.log(user.address?.city); // undefined (no error thrown)
console.log(user.name?.toUpperCase()); // "ALICE" (name exists, method runs)
If any part of the chain is null or undefined, ?. short-circuits and returns undefined instead of throwing. The rest of the chain is never evaluated.
It also works with method calls and array access:
let user = {
getProfile: null,
orders: [{ id: 1 }, { id: 2 }]
};
// Calling a method that might not exist:
console.log(user.getProfile?.()); // undefined (no error)
// Accessing array items that might not exist:
console.log(user.orders?.[0]?.id); // 1
console.log(user.orders?.[5]?.id); // undefined (index 5 doesn't exist)
Combining ?. with ??
These two operators are natural partners. Use ?. to safely access a value that might not exist, and ?? to provide a fallback when it doesn't:
let user1 = { profile: null };
let name1 = user1.profile?.displayName ?? "Anonymous";
console.log(name1); // "Anonymous"
let user2 = { profile: { displayName: "Daniyal" } };
let name2 = user2.profile?.displayName ?? "Anonymous";
console.log(name2); // "Daniyal"
You'll see this combination constantly in modern JavaScript, especially when working with APIs where some fields are optional. It's clean, safe, and expressive. Once you start using it, you won't want to go back to the old guard-check style.
09Operator Precedence: The Order JavaScript Uses to Evaluate Expressions
Operator precedence determines the order in which JavaScript evaluates parts of an expression, similar to how multiplication happens before addition in standard math. But in JavaScript, the rules go beyond just arithmetic.
console.log(2 + 3 * 4); // 14, not 20
// 3 * 4 runs first (multiplication has higher precedence than addition)
// then 2 + 12 = 14
Here's the order of the operators we've covered in this lesson, from highest precedence (runs first) to lowest (runs last):
- Parentheses
(): always highest priority - Member access and optional chaining:
.and?. - Unary operators:
!,++,--,typeof - Exponentiation:
** - Multiplication, Division, Remainder:
*,/,% - Addition and Subtraction:
+,- - Comparison:
<,>,<=,>= - Equality:
==,===,!=,!== - Logical AND:
&& - Logical OR:
|| - Nullish coalescing:
?? - Assignment:
=,+=,-=, and all compound assignment operators
Notice that && has higher precedence than ||. This means:
// Without parentheses:
console.log(true || false && false);
// Evaluated as: true || (false && false)
// Which is: true || false
// Result: true
// This is the same as if you'd written:
console.log(true || (false && false)); // true
When in doubt, use parentheses. They cost nothing and make intent crystal clear:
// Hard to parse at a glance:
let result = 2 + 3 * 4 > 10 && !false;
// Much clearer with explicit grouping:
let result2 = ((2 + (3 * 4)) > 10) && (!false);
// Both give the same result, but the second one you can follow step by step
Also: ?? and || cannot be mixed directly without parentheses. JavaScript throws a SyntaxError if you try. This is intentional: it prevents ambiguous expressions:
// SyntaxError: Cannot mix ?? with || or && without parentheses
// let x = a ?? b || c;
// Fix: use parentheses to clarify your intent:
let a = null;
let x = (a ?? "nullish-fallback") || "falsy-fallback";
console.log(x); // "nullish-fallback"
Operator Precedence: Step-by-Step Breakdown
10Bringing It All Together: A Real-World Example
Let's build something practical that uses everything we covered. Here's a function that processes a user object from an API response, the kind of thing you'd actually write in a real project:
function buildUserProfile(apiResponse) {
// Optional chaining + nullish coalescing: safely access nested properties with fallbacks
let name = apiResponse?.user?.displayName ?? "Anonymous";
let age = apiResponse?.user?.age ?? "Not provided";
let email = apiResponse?.user?.contact?.email ?? "No email on file";
let isVerified = apiResponse?.user?.verified ?? false;
let score = apiResponse?.user?.score ?? 0;
// Arithmetic: calculate what score is needed for the next tier
let pointsToNextTier = Math.ceil(score * 1.5) - score + 100;
// Logical AND: both conditions must be true for premium access
let canAccessPremium = isVerified && score >= 500;
// Logical OR: either condition grants the badge
let showLoyaltyBadge = isVerified || score > 1000;
// Strict equality + ternary (comparison): determine rank
let rank = score >= 1000 ? "Gold"
: score >= 500 ? "Silver"
: "Bronze";
// Strict equality: check if brand new account
let isNewUser = score === 0;
return {
name,
age,
email,
isVerified,
score,
pointsToNextTier,
canAccessPremium,
showLoyaltyBadge,
rank,
isNewUser
};
}
// Full API response:
let fullResponse = {
user: {
displayName: "Alice",
age: 28,
verified: true,
score: 750,
contact: { email: "[email protected]" }
}
};
console.log(buildUserProfile(fullResponse));
/*
{
name: "Alice",
age: 28,
email: "[email protected]",
isVerified: true,
score: 750,
pointsToNextTier: 325,
canAccessPremium: true,
showLoyaltyBadge: true,
rank: "Silver",
isNewUser: false
}
*/
// Partial response (missing fields):
let partialResponse = {
user: { verified: false, score: 0 }
};
console.log(buildUserProfile(partialResponse));
/*
{
name: "Anonymous",
age: "Not provided",
email: "No email on file",
isVerified: false,
score: 0,
pointsToNextTier: 100,
canAccessPremium: false,
showLoyaltyBadge: false,
rank: "Bronze",
isNewUser: true
}
*/
That single function uses arithmetic, strict equality, logical AND and OR, optional chaining, and nullish coalescing, all working together. And notice: because we used ?? instead of || for the score default, a score of 0 is correctly kept as 0 instead of being silently replaced.
11What's Next in the Series
Next up is destructuring: the elegant way to unpack values from arrays and objects in JavaScript. Once you learn it, you start using it in almost every function you write. We'll cover array destructuring, object destructuring, default values on extraction, renaming variables, nested destructuring, and how to use it in function parameters.
If you want to revisit anything from this lesson, spend time with the === vs == section and the nullish coalescing demo. Those are the two topics where understanding the nuance protects you from real bugs in real code.
Operators are everywhere. Now you know exactly what they're doing when you see them.
JavaScript Operators
Loading challenge…
Ready to Practice?
Select a difficulty level above to load your challenge.