<< Home

Chapter 5: Operators and Expressions

JavaScript programs are built from expressions that produce values, and operators that combine or transform them. Whether performing arithmetic, testing equality, chaining logic, or managing control flow, operators define how values interact and how results are derived. Understanding these relationships is essential to writing predictable and efficient code.

This chapter explores JavaScript’s wide range of operators: from basic arithmetic and assignment, through comparison and logical operations, to newer features like the nullish coalescing and optional chaining operators. You will also learn how operator precedence affects the order of evaluation, and how to use grouping to make intentions clear.

💡 In JavaScript, nearly everything that produces a value is an expression. Even something as simple as a function call or a ternary condition can act as an expression within a larger statement.

Operators form the connective tissue of the language. They turn individual values into relationships, enable decision-making, and allow concise manipulation of data. Mastering them lays the groundwork for confident, expressive coding throughout all your JavaScript projects.

Arithmetic and assignment

Arithmetic operators are among the most familiar in JavaScript, allowing you to perform standard mathematical operations such as addition, subtraction, multiplication, division, and remainder. These operators work with both numbers and, in some cases, strings that can be converted to numbers.

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.333...
console.log(a % b);  // 1

JavaScript also supports the unary increment (++) and decrement (--) operators, which add or subtract one from a variable. These can appear before or after the variable, depending on whether you want the change to occur before or after the value is returned.

let count = 5;

console.log(++count); // 6 (increment, then return)
console.log(count--); // 6 (return, then decrement)
console.log(count);   // 5
⚠️ Avoid using ++ and -- inside complex expressions. Their side effects can make code harder to read and reason about, especially when used within loops or conditionals.

Assignment operators

Assignment operators let you store values in variables, with = being the most basic form. However, JavaScript provides compound assignment operators that combine assignment with arithmetic for brevity and clarity.

let x = 10;

x += 5;  // x = x + 5 → 15
x -= 2;  // x = x - 2 → 13
x *= 3;  // x = x * 3 → 39
x /= 3;  // x = x / 3 → 13
x %= 5;  // x = x % 5 → 3
💡 Compound assignment operators not only make code shorter but also help signal your intent clearly. When readers see += or *=, they instantly know the variable is being updated relative to its current value.

Exponentiation

JavaScript includes the exponentiation operator (**) for raising a value to a power. It is right-associative, which means that in chained operations it evaluates from right to left.

console.log(2 ** 3);       // 8
console.log(2 ** 3 ** 2);  // 512 (2 ** (3 ** 2))
⚠️ The exponentiation operator was introduced in ECMAScript 2016. Older code may use Math.pow() for the same effect.

Unary plus and minus

The unary plus (+) converts its operand to a number if possible, while unary minus (-) negates it after conversion. This makes them useful for ensuring numeric types before performing calculations.

let n = "42";

console.log(+n);  // 42 (string converted to number)
console.log(-n);  // -42
💡 Using + as a quick conversion to number can be more concise than Number(), but be cautious: it can produce NaN if the value cannot be converted.

Comparison and equality

Comparison operators evaluate the relationship between two values and return a Boolean result (true or false). They are fundamental to decision-making in JavaScript, allowing programs to branch and react to data conditions.

let a = 10;
let b = 20;

console.log(a < b);   // true
console.log(a > b);   // false
console.log(a <= b);  // true
console.log(a >= b);  // false

These operators work on numbers, strings, and other comparable types. When comparing strings, JavaScript uses Unicode code point order, so comparisons are case-sensitive and may not align with natural language sorting.

console.log("apple" < "banana");  // true
console.log("Zoo" < "apple");     // false
⚠️ String comparisons are lexicographic, meaning they compare character codes rather than alphabetic or locale-based ordering. For user-facing text sorting, use localeCompare() instead.

Equality vs identity

JavaScript has two types of equality comparison: loose equality (==) and strict equality (===). The difference lies in whether type conversion occurs before comparison.

console.log(5 == "5");   // true  (values are coerced)
console.log(5 === "5");  // false (types differ)

Loose equality attempts to convert operands to a common type before comparing. This can lead to unexpected results due to JavaScript’s coercion rules. Strict equality compares both value and type directly, making it the safer and more predictable option.

💡 As a rule of thumb, always use === and !== unless you have a specific reason to allow type coercion.

Inequality operators

Just as with equality, JavaScript provides both loose (!=) and strict (!==) forms of inequality.

console.log(5 != "5");   // false (values coerced)
console.log(5 !== "5");  // true (types differ)

These behave exactly like their equality counterparts, differing only in the negation of the result.

Object and reference comparison

When comparing objects, arrays, or functions, JavaScript checks whether both operands reference the same underlying object, not whether their contents are identical.

let x = [1, 2, 3];
let y = [1, 2, 3];
let z = x;

console.log(x === y); // false (different objects)
console.log(x === z); // true (same reference)
⚠️ Objects and arrays are compared by reference, not by structure or value. To compare their contents, you must check properties or elements manually, or use a helper function.

Comparing null and undefined

The values null and undefined are equal to each other under loose equality but not under strict equality.

console.log(null == undefined);   // true
console.log(null === undefined);  // false

This subtle distinction is a common source of confusion. In modern code, prefer strict comparisons or use the nullish coalescing operator (??) when you need to handle both null and undefined together.

💡 If you need to test for either null or undefined, you can use value == null safely, since it will match both and nothing else.

Logical operators and short-circuiting

Logical operators combine or negate Boolean values, allowing expressions to express multiple conditions or default behaviors. In JavaScript, the main logical operators are && (AND), || (OR), and ! (NOT). They return one of their operands rather than a strict Boolean value, which makes them especially powerful for control flow and value selection.

let a = true;
let b = false;

console.log(a && b);  // false
console.log(a || b);  // true
console.log(!a);      // false

The && operator returns the first falsy value or the last truthy value if all are true. The || operator does the opposite: it returns the first truthy value or the last falsy value if none are true. Because they return operand values rather than simple true or false, they are often used for short-circuiting and fallback expressions.

let username = "";
let display = username || "Guest";
console.log(display);  // "Guest"
💡 Short-circuiting means that evaluation stops as soon as the result is known. In a && b, if a is falsy, b is never evaluated. In a || b, if a is truthy, b is skipped.

Truthy and falsy values

Any value in JavaScript can be treated as either true or false when evaluated in a Boolean context. Values considered falsy include false, 0, -0, "" (empty string), null, undefined, and NaN. Everything else is truthy.

if ("text") {
  console.log("Truthy");
}

if (0) {
  console.log("Falsy");  // not executed
}
⚠️ Be cautious when relying on truthiness for control flow. For example, 0 and "" are falsy, which can lead to unintended results in conditions that test for presence or length.

Logical assignment operators

ES2021 introduced logical assignment operators that combine logical evaluation with assignment. These allow a variable to be updated only when certain logical conditions apply.

let user = null;
let defaultUser = "Anonymous";

user ||= defaultUser;  // assigns if user is falsy
console.log(user);     // "Anonymous"

let config = { debug: true };
config &&= { verbose: true };  // assigns if config is truthy
console.log(config);           // { verbose: true }

let count = 0;
count ??= 10;        // assigns if count is null or undefined
console.log(count);  // 0 (not changed)
💡 Logical assignment operators (&&=, ||=, and ??=) are concise ways to update variables conditionally without writing explicit if statements.

Using logic for control and defaults

Because logical operators return operand values, they can act as elegant control structures or defaulting mechanisms within expressions. For instance, || is often used to provide a fallback value, while && can execute code conditionally.

let debug = true;

debug && console.log("Debug mode active");

let port = userPort || 8080; // use default if undefined
⚠️ While logical operators are compact and expressive, overusing them in complex expressions can reduce readability. For clarity, prefer explicit conditionals when the logic becomes difficult to parse.

Nullish coalescing and optional chaining

Modern JavaScript introduces two powerful operators that make working with uncertain data safer and more expressive: the nullish coalescing operator (??) and the optional chaining operator (?.). Both were added in ECMAScript 2020 and are especially useful when dealing with optional data structures, configuration objects, or JSON responses where some values may not exist.

The ?? (nullish coalescing) operator

The nullish coalescing operator returns the right-hand operand only when the left-hand operand is null or undefined. This differs from the logical OR operator (||), which treats all falsy values (such as 0, "", or false) as triggers for the fallback.

let count = 0;

console.log(count || 10);  // 10 (because 0 is falsy)
console.log(count ?? 10);  // 0  (because 0 is not null or undefined)

Use ?? when you only want to provide a default for truly missing values, not for all falsy values.

💡 The nullish coalescing operator is ideal when handling default values in user input, API data, or configuration files. It ensures that valid but falsy values like 0 or "" are preserved.

The ?. (optional chaining) operator

The optional chaining operator allows you to safely access deeply nested properties without causing a runtime error if an intermediate value is null or undefined. Instead of throwing an exception, the expression simply evaluates to undefined.

let user = {
  profile: {
    name: "Robin"
  }
};

console.log(user.profile?.name);   // "Robin"
console.log(user.account?.email);  // undefined
console.log(user.account.email);   // TypeError

Optional chaining can also be used for method calls and array indexing, which makes it particularly versatile when navigating uncertain data structures.


user.getSettings?.();  // calls only if getSettings exists
let theme = user.prefs?.themes?.[0];
⚠️ The optional chaining operator only suppresses errors for null or undefined. If a property exists but its access triggers another kind of error (such as a thrown exception), that error still propagates.

Combining ?. and ??

These two operators often work best together: optional chaining safely accesses a value, and nullish coalescing provides a fallback if that value is missing.

let config = {
  database: {
    host: "localhost"
  }
};

let host = config.database?.host ?? "default.local";
let port = config.database?.port ?? 3306;

console.log(host);  // "localhost"
console.log(port);  // 3306
💡 Combining ?. and ?? helps create resilient expressions that gracefully handle missing or incomplete data, especially when working with external sources such as APIs or user-submitted content.

Together, nullish coalescing and optional chaining reduce the need for repetitive if checks and defensive coding. They make your logic cleaner, safer, and easier to maintain when navigating unpredictable object hierarchies.

Bitwise and exponentiation

JavaScript includes a set of bitwise operators that perform operations on numbers at the binary level. These operators treat operands as 32-bit signed integers, working directly on their bit representations rather than on their numeric values. Although bitwise operations are less common in everyday JavaScript, they are useful in performance-sensitive code, low-level data manipulation, and certain algorithmic techniques such as masking and flags.

Bitwise fundamentals

Bitwise operators first convert their operands to 32-bit integers, perform the specified operation on each bit, and then return a new 32-bit integer. The main bitwise operators are as follows:

let a = 5;  // 0101
let b = 3;  // 0011

console.log(a & b);  // 1  (0001)
console.log(a | b);  // 7  (0111)
console.log(a ^ b);  // 6  (0110)
console.log(~a);     // -6 (bitwise NOT)

Bitwise shifts move bits to the left or right. Left shifts fill with zeros on the right, while right shifts can either preserve the sign bit (>>) or fill with zeros (>>>).

let value = 8;  // 0000 1000

console.log(value << 1);  // 16  (0001 0000)
console.log(value >> 1);  // 4   (0000 0100)
console.log(value >>> 1); // 4   (0000 0100)
⚠️ Bitwise operations always yield 32-bit signed integers. If you use them on very large numbers, they may truncate data or wrap around unexpectedly due to overflow.

Practical uses of bitwise operators

Bitwise logic can be used to represent multiple Boolean states within a single number, a technique known as bit masking. Each bit acts as a flag that can be set, cleared, or checked efficiently.

const READ  = 1;  // 0001
const WRITE = 2;  // 0010
const EXEC  = 4;  // 0100

let permissions = READ | WRITE;   // 0011

console.log(permissions & READ);  // true (bit set)
console.log(permissions & EXEC);  // false (bit not set)
💡 Bit masking remains common in file permissions, graphics, and low-level protocols. Even though modern JavaScript rarely needs it, understanding bitwise logic gives insight into how systems represent complex states compactly.

Exponentiation revisited

The exponentiation operator (**) raises a number to a given power. It is right-associative, meaning that expressions with multiple exponentiations evaluate from right to left.

console.log(2 ** 3);       // 8
console.log(2 ** 3 ** 2);  // 512  → 2 ** (3 ** 2)

This operator is equivalent to Math.pow() but more concise and readable. It can also be combined with assignment as **= to apply exponentiation in place.

let n = 4;
n **= 3;
console.log(n);  // 64
⚠️ Because exponentiation grows rapidly, use it with care in loops or recursive functions. Large powers can easily exceed JavaScript’s Number.MAX_SAFE_INTEGER.

Although bitwise operations and exponentiation are rarely used together, both give fine-grained control over numerical behavior. They represent JavaScript’s bridge between high-level convenience and low-level precision.

Operator precedence and grouping

When multiple operators appear in a single expression, JavaScript follows a defined order of evaluation known as operator precedence. Operators with higher precedence execute before those with lower precedence. Understanding this order prevents subtle bugs and makes complex expressions predictable.

If operators share the same precedence level, JavaScript applies associativity rules to determine whether evaluation proceeds from left to right or right to left. Most operators are left-associative, but assignment and exponentiation are notable exceptions since they evaluate from right to left.

💡 Parentheses () always take precedence and can be used to make evaluation order explicit. When in doubt, group sub-expressions for clarity.

Highest to lowest precedence

OperatorDescription
()Parentheses for grouping and function calls
[], ., ?.Array indexing, property access, and optional chaining
new (with arguments)Object instantiation when called with parentheses
++, --Postfix increment and decrement
++, --, +, -, ~, !, typeof, void, deletePrefix operations, unary plus/minus, bitwise NOT, logical NOT, type queries, and property deletion
**Exponentiation (right-associative)
*, /, %Multiplication, division, remainder
+, -Addition and subtraction (numeric or string concatenation)
<<, >>, >>>Bitwise shift operators
<, <=, >, >=, in, instanceofRelational comparisons and membership/type checking
==, !=, ===, !==Equality and inequality comparisons
&Bitwise AND
^Bitwise XOR
|Bitwise OR
&&Logical AND (short-circuiting)
||Logical OR (short-circuiting)
??Nullish coalescing (short-circuiting on null or undefined)
? :Ternary conditional operator
=, +=, -=, *=, /=, %=, **=, <<=, >>=, >>>=, &=, |=, ^=, ||=, &&=, ??=Assignment and compound assignment (right-associative)
,Comma operator (evaluates expressions in sequence, returning the last value)

Grouping for clarity

Even though operator precedence determines evaluation order, adding parentheses often makes expressions easier to read and maintain. Parentheses do not change results when used correctly, but they remove ambiguity for readers and tools alike.

// Without grouping
let result = a + b * c;

// With grouping for clarity
let result = (a + b) * c;
⚠️ Complex expressions that rely heavily on precedence can obscure meaning. For clean and maintainable code, use parentheses to make intent obvious, even if you know the precedence rules.

Operator precedence and grouping shape how JavaScript interprets every expression. A clear grasp of these rules helps prevent logical errors and ensures that your code executes exactly as you intend.