Skip to main content

Comparison operators & equality

Use strict equality, understand truthy/falsy coercion, and avoid unnecessary complexity in conditions.

15.1 Use === and !==

eslint: eqeqeq Use === and !== over == and !=.

15.2 How conditional coercion works

Conditional statements such as the if statement evaluate their expression using coercion with the ToBoolean abstract method and always follow these simple rules:
  • Objects evaluate to true
  • Undefined evaluates to false
  • Null evaluates to false
  • Booleans evaluate to the value of the boolean
  • Numbers evaluate to false if +0, -0, or NaN, otherwise true
  • Strings evaluate to false if an empty string '', otherwise true
if ([0] && []) {
  // true
  // an array (even an empty one) is an object, objects will evaluate to true
}
An empty array [] is an object and evaluates to true. This surprises many developers who expect it to be falsy like an empty string.

15.3 Use shortcuts for booleans, explicit comparisons for strings and numbers

if (isValid) {
  // ...
}

if (name !== '') {
  // ...
}

if (collection.length > 0) {
  // ...
}

15.5 Use braces for case clauses with lexical declarations

eslint: no-case-declarations Use braces to create blocks in case and default clauses that contain lexical declarations (e.g. let, const, function, and class). Lexical declarations are visible in the entire switch block but only get initialized when assigned, which only happens when its case is reached. This causes problems when multiple case clauses attempt to define the same thing.
switch (foo) {
  case 1: {
    let x = 1;
    break;
  }
  case 2: {
    const y = 2;
    break;
  }
  case 3: {
    function f() {
      // ...
    }
    break;
  }
  case 4:
    bar();
    break;
  default: {
    class C {}
  }
}
Without braces, a let or const declared in one case is technically visible in all other cases in the same switch block, leading to confusing ReferenceErrors.

15.6 Don’t nest ternaries

eslint: no-nested-ternary Ternaries should not be nested and generally be single line expressions.
// split into 2 separated ternary expressions
const maybeNull = value1 > value2 ? 'baz' : null;

// better
const foo = maybe1 > maybe2
  ? 'bar'
  : maybeNull;

// best
const foo = maybe1 > maybe2 ? 'bar' : maybeNull;

15.7 Avoid unneeded ternary statements

eslint: no-unneeded-ternary
const foo = a || b;
const bar = !!c;
const baz = !c;
const quux = a ?? b;

15.8 Enclose mixed operators in parentheses

eslint: no-mixed-operators When mixing operators, enclose them in parentheses. The only exception is the standard arithmetic operators +, -, and ** since their precedence is broadly understood. We recommend enclosing / and * in parentheses because their precedence can be ambiguous when they are mixed. This improves readability and clarifies the developer’s intention.
const foo = (a && b < 0) || c > 0 || (d + 1 === 0);

const bar = a ** b - (5 % d);

if (a || (b && c)) {
  return d;
}

const bar = a + (b / c) * d;

15.9 Use the nullish coalescing operator ?? precisely

The nullish coalescing operator (??) returns its right-hand side operand when its left-hand side operand is null or undefined. Otherwise, it returns the left-hand side operand. It provides precision by distinguishing null/undefined from other falsy values, enhancing code clarity and predictability.
const value = null ?? 'default';
// returns 'default'

const user = {
  name: 'John',
  age: null
};
const age = user.age ?? 18;
// returns 18
Use ?? when you want to fall back only for null or undefined. Use || when you want to fall back for any falsy value (including 0, '', and false).

Blocks

Consistent block formatting prevents ambiguity in multi-statement control flow.

16.1 Use braces with all multiline blocks

eslint: nonblock-statement-body-position
if (test) return false;

// or
if (test) {
  return false;
}

function bar() {
  return false;
}

16.2 Put else on the same line as the closing brace

eslint: brace-style If you’re using multiline blocks with if and else, put else on the same line as your if block’s closing brace.
if (test) {
  thing1();
  thing2();
} else {
  thing3();
}

16.3 Omit else after a returning if block

eslint: no-else-return If an if block always executes a return statement, the subsequent else block is unnecessary. A return in an else if block following an if block that contains a return can be separated into multiple if blocks.
function foo() {
  if (x) {
    return x;
  }

  return y;
}

function cats() {
  if (x) {
    return x;
  }

  if (y) {
    return y;
  }
}

function dogs(x) {
  if (x) {
    if (z) {
      return y;
    }
  } else {
    return z;
  }
}

Control statements

Keep condition lines readable and use if statements instead of short-circuit operators for side effects.

17.1 Break long conditions onto new lines

In case your control statement (if, while etc.) gets too long or exceeds the maximum line length, each (grouped) condition can be put on a new line. The logical operator should begin the line. Requiring operators at the beginning of the line keeps the operators aligned and follows a pattern similar to method chaining. This also improves readability by making it easier to visually follow complex logic.
if (
  foo === 123
  && bar === 'abc'
) {
  thing1();
}

if (
  (foo === 123 || bar === 'abc')
  && doesItLookGoodWhenItBecomesThatLong()
  && isThisReallyHappening()
) {
  thing1();
}

if (foo === 123 && bar === 'abc') {
  thing1();
}

17.2 Don’t use selection operators in place of control statements

if (!isRunning) {
  startRunning();
}
Using && or || for side effects is clever but obscures intent. Reserve these operators for expressions, and use if statements for imperative control flow.

Build docs developers (and LLMs) love