Operator precedence and associativity

Multiple operations in a sequence

Multiple operations can be used in a sequence. The same operators or different operators can be used in the sequence. In what order are these expressions executed? In the next example the first sequence involves multiple equal operators (??). The following two sequences involve multiple different operators (?? and +). One uses parentheses, the other does not.


const a = undefined,
      b = null,
      c = "Hello";

console.log(a ?? b ?? c ?? "none"); // logs: "Hello"
console.log((a ?? "One") + (b ?? "Two") + (c ?? "Three")); // logs: "OneTwoHello"
console.log(a ?? "One" + b ?? "Two" + c ?? "Three"); // logs: "Onenull" 	

Operator precedence

Operator precedence determines the order of operations when evaluating a given expression. Operators with higher precedence become the operands of operators with lower precedence.

As in mathematics, exponentiation has higher precedence than both multiplication and division and they, in turn, have both higher precedence than both addition and subtraction. Comparison operators have higher precedence over logical operators. This operator precedence table lists all operators in order from highest precedence to lowest precedence.


let varA, varB, x = 9, y = 11;
console.log(2 + 3 * 5); // logs: 17 // 2 + (3 * 5) = 2 + 15 = 17
console.log(50 - 4 * 3 ** 2); // logs: 14 // 50 - (4 * (3 ** 2)) = 50 - (4 * 9) = 50 - 36 = 14.
console.log(x < 10 && y > 10); // logs: true // (x < 10) && (y > 10) // "true && true" returns true

console.log(varA ?? 2 * varB ?? 3); // logs: NaN // varA ?? (2 * varB) ?? 3
console.log( (varA ?? 2) * (varB ?? 3) ); // logs: 6 // 2 * 3

The precedence can be changed by using parentheses. Parts of an expression in parentheses are interpreted as an operand on its own.


let result = 0;
console.log( 40-(4*(2+(2**3))) === result &&
             40-(4*(2+8)) === result &&
             40-(4*10) === result ); // logs: true

Operator associativity

With operators on different precedence levels, associativity does not matter. Associativity only comes into play when there are multiple operators of the same precedence. In the operator precedence table there is also a column indicating the associativity of each operator. It indicates the "direction of processing". Left-associative operators are processed left-to-right (as (a OP1 b) OP2 c) and right-associative operators are processed right-to-left (as a OP1 (b OP2 c)). Operators on the same precedence level all have the same associativity (if associativity is applicable).


console.log( 5-2-1 === (5-2)-1 ); // logs: true // subtraction is left-associative
console.log( 2**3**4 === 2**(3**4) ); // logs: true // Exponentiation is right-associative
console.log( 5-2-1 === 5-(2-1) ); // logs: false
console.log( 2**3**4 === (2**3)**4 ); // logs: false

console.log(5 - 3 + 2); // logs: 4 // same precedence level, both left-associative: (5 - 3) + 2 = 2 + 2 = 4.

console.log(1 < 3 > 2); // logs: false // same precedence level, both left-associative: 1 < 3 is true, which is converted to 1; 1 > 2 is false.
console.log( 1 < 3 && 3 > 2 ); // logs: true // different precedence levels: (1 < 3) && (3 > 2)

let varA, varB;
console.log(varA = varB = 5); // logs: 5 // Assignment operators are right-associative: varA = (varB = 5) thus it logs the return value of varB = 5.

Expressions in JS are always evaluated left-to-right, regardless of associativity and precedence. In the next example the function calls are executed left-to-right, but the exponentiation (2**(3**2)) is processed right-to-left, according to its associativity.


function operand(id, value){
  console.log("Evaluating function " + id);
  return value;
}

console.log( operand("one",2) ** operand("two",3) ** operand("three",2) ); // 2**(3**2) = 2**9 = 512.
Output to the console:

Evaluating function one
Evaluating function two
Evaluating function three
512

Although we could rely on the precedence and associativity rules, using parentheses to explicitly define the order of operations generally results in clearer code and is generally considered best practice.

However, what is within parentheses is not always evaluated first when dealing with short-circuit evaluation.


let a = true;
console.log( a || (b * c) ); // logs: true // b * c was not evaluated at all, even though it is in parentheses