Skip to main content

How to Use Logical Operators in JavaScript: OR, AND, NOT, and Nullish Coalescing

Logical operators are the decision-making tools of JavaScript. They combine conditions, determine default values, guard against null references, and control which parts of your code execute. While they may look simple on the surface, JavaScript's logical operators behave differently from their counterparts in most other programming languages. They do not always return true or false. Instead, they return actual values, a feature that enables powerful patterns but also creates subtle traps.

This guide covers every logical operator in JavaScript, including the modern nullish coalescing operator and logical assignment operators. You will learn exactly what each operator returns, how short-circuit evaluation works, and the critical difference between || and ?? that prevents one of the most common bugs in JavaScript.

OR || : First Truthy Value (Short-Circuit Evaluation)

In many languages, the OR operator returns a boolean. In JavaScript, the OR operator || returns the first truthy value it encounters, or the last value if none are truthy. It evaluates operands from left to right and stops as soon as it finds a truthy value.

Basic Boolean Usage

When used with boolean values, || behaves exactly as you would expect:

console.log(true || true);    // true
console.log(true || false); // true
console.log(false || true); // true
console.log(false || false); // false

The Real Behavior: Returning Values, Not Booleans

Here is where JavaScript differs from most languages. The || operator does not convert its result to a boolean. It returns the actual operand value:

console.log(1 || 0);                        // 1          (first truthy value)
console.log(0 || 1); // 1 (first truthy value)
console.log(null || "hello"); // "hello" (first truthy value)
console.log(0 || "" || null || "found it"); // "found it"
console.log(undefined || 0 || "" || null); // null (all falsy, returns last)

The algorithm works like this:

  1. Evaluate each operand from left to right
  2. Convert each to boolean
  3. If the result is true, stop and return the original (unconverted) value
  4. If all operands are falsy, return the last operand
// Step-by-step: 0 || "" || null || "hello" || 42
// 0 → falsy, continue
// "" → falsy, continue
// null → falsy, continue
// "hello" → truthy, STOP and return "hello"
// 42 is never evaluated

console.log(0 || "" || null || "hello" || 42); // "hello"

Short-Circuit Evaluation

"Short-circuit" means that once the result is determined, the remaining operands are not evaluated at all. This is not just a performance optimization; it has practical consequences:

let called = false;

function sideEffect() {
called = true;
return "result";
}

true || sideEffect(); // sideEffect() is NEVER called
console.log(called); // false

false || sideEffect(); // sideEffect() IS called
console.log(called); // true

Practical Uses of ||

Default values (with a caveat):

function greet(name) {
name = name || "Guest";
console.log(`Hello, ${name}!`);
}

greet("Alice"); // "Hello, Alice!"
greet(undefined); // "Hello, Guest!" (undefined is falsy)
greet(null); // "Hello, Guest!" (null is falsy)
greet(""); // "Hello, Guest!" (PROBLEM! Empty string is falsy too)
greet(0); // "Hello, Guest!" (PROBLEM! 0 is falsy too)

We will address this problem in the ?? section below.

First available value from multiple sources:

let userPreference = null;
let systemDefault = undefined;
let fallback = "en-US";

let language = userPreference || systemDefault || fallback;
console.log(language); // "en-US"

Conditional execution (short-circuit as a pattern):

let isDebugMode = true;

// If isDebugMode is truthy, run the function
isDebugMode || console.log("This won't run - true found first");
isDebugMode && console.log("Debug: Application started"); // Runs

However, this pattern is less readable than a simple if statement. Prefer if for executing side effects.

AND &&: First Falsy Value (Short-Circuit Evaluation)

The AND operator && is the mirror image of ||. It returns the first falsy value it encounters, or the last value if all are truthy. It evaluates from left to right and stops as soon as it finds a falsy value.

Basic Boolean Usage

console.log(true && true);    // true
console.log(true && false); // false
console.log(false && true); // false
console.log(false && false); // false

Returning Values, Not Booleans

Just like ||, the && operator returns the actual value, not a boolean:

console.log(1 && 2);              // 2        -> all truthy, returns last
console.log(1 && 2 && 3); // 3 -> all truthy, returns last
console.log(1 && 0 && 3); // 0 -> first falsy value
console.log("hello" && "world"); // "world" -> all truthy, returns last
console.log("hello" && 0); // 0 -> first falsy value
console.log(null && "hello"); // null -> first falsy value, "hello" never evaluated
console.log(0 && null); // 0 -> first falsy value

The algorithm:

  1. Evaluate each operand from left to right
  2. Convert each to boolean
  3. If the result is false, stop and return the original (unconverted) value
  4. If all operands are truthy, return the last operand
// Step-by-step: "Alice" && 42 && null && "unreachable"
// "Alice" → truthy, continue
// 42 → truthy, continue
// null → falsy, STOP and return null
// "unreachable" is never evaluated

console.log("Alice" && 42 && null && "unreachable"); // null

Short-Circuit with &&

let user = null;

// Safe property access using &&
let name = user && user.name;
console.log(name); // null -> user is falsy, user.name never evaluated

user = { name: "Alice" };
name = user && user.name;
console.log(name); // "Alice" -> user is truthy, returns user.name

This was the standard pattern before optional chaining (?.) was introduced:

// Old pattern (still works, still common)
let city = user && user.address && user.address.city;
// Modern pattern (cleaner)
let city = user?.address?.city;

Practical Uses of &&

Conditional rendering (common in React):

let isLoggedIn = true;
let username = "Alice";

// If isLoggedIn is truthy, the expression after && is evaluated
let greeting = isLoggedIn && `Welcome, ${username}!`;
console.log(greeting); // "Welcome, Alice!"
let isLoggedIn = false;
let greeting = isLoggedIn && `Welcome, ${username}!`;
console.log(greeting); // false -> stopped at first falsy value

Guard conditions:

// Only call the function if the array exists and has items
let items = [1, 2, 3];
items && items.length && console.log(`${items.length} items found`);
// "3 items found"

// Prefer if statements for clarity
if (items && items.length) {
console.log(`${items.length} items found`);
}
info

While && can be used as a substitute for if statements, most style guides recommend using actual if statements for executing side effects. Reserve && short-circuiting for value expressions, not for control flow.

NOT ! : Boolean Inversion and Double NOT !!

The NOT operator ! is the only logical operator that always returns a boolean. It converts its operand to boolean and then inverts it.

Basic Usage

console.log(!true);   // false
console.log(!false); // true

NOT with Non-Boolean Values

! first converts the value to boolean (using the truthy/falsy rules), then inverts:

// Truthy values → false
console.log(!"hello"); // false (string is truthy → true → inverted to false)
console.log(!42); // false
console.log(!{}); // false
console.log(![]); // false

// Falsy values → true
console.log(!""); // true (empty string is falsy → false → inverted to true)
console.log(!0); // true
console.log(!null); // true
console.log(!undefined); // true
console.log(!NaN); // true

Double NOT !! for Boolean Conversion

Applying ! twice converts any value to its boolean equivalent:

console.log(!!"hello");    // true   ("hello" → false → true)
console.log(!!42); // true (42 → false → true)
console.log(!!""); // false ("" → true → false)
console.log(!!0); // false (0 → true → false)
console.log(!!null); // false (null → true → false)
console.log(!!undefined); // false
console.log(!!NaN); // false
console.log(!![]); // true ([] is truthy)
console.log(!!{}); // true ({} is truthy)

!!value is equivalent to Boolean(value). Both produce the same result:

console.log(Boolean("hello"));  // true
console.log(!!"hello"); // true (shorter, same result)

console.log(Boolean(0)); // false
console.log(!!0); // false (shorter, same result)

Practical Uses of !

// Toggling a boolean
let isVisible = true;
isVisible = !isVisible;
console.log(isVisible); // false
isVisible = !isVisible;
console.log(isVisible); // true

// Checking for empty/missing values
let input = "";
if (!input) {
console.log("Input is empty or missing");
}

// Converting to boolean for an API or storage
let hasItems = !!cart.length; // true if cart has items, false if empty

The Nullish Coalescing Operator ??

The nullish coalescing operator ?? was introduced in ES2020. It returns the right-hand operand when the left-hand operand is null or undefined, and returns the left-hand operand otherwise.

Basic Syntax

let result = leftValue ?? rightValue;
// If leftValue is null or undefined → returns rightValue
// If leftValue is anything else → returns leftValue

How It Works

console.log(null ?? "default");       // "default"  (null triggers fallback)
console.log(undefined ?? "default"); // "default" (undefined triggers fallback)

console.log(0 ?? "default"); // 0 (0 is NOT null/undefined)
console.log("" ?? "default"); // "" ("" is NOT null/undefined)
console.log(false ?? "default"); // false (false is NOT null/undefined)
console.log(NaN ?? "default"); // NaN (NaN is NOT null/undefined)

The key insight: ?? only cares about null and undefined. It does not treat 0, "", false, or NaN as "missing" values.

Practical Usage

// User settings with proper defaults
function createUser(options) {
let name = options.name ?? "Anonymous";
let age = options.age ?? "Unknown";
let score = options.score ?? 0;
let bio = options.bio ?? "No bio provided";

return { name, age, score, bio };
}

// Score of 0 is preserved, not replaced with default
let user = createUser({ name: "Alice", score: 0, bio: "" });
console.log(user);
// { name: "Alice", age: "Unknown", score: 0, bio: "" }
// score is 0 (not replaced), bio is "" (not replaced)

Chaining ??

let userPref = null;
let systemPref = undefined;
let defaultValue = "English";

let language = userPref ?? systemPref ?? defaultValue;
console.log(language); // "English"
// With a valid preference at any level
let userPref = "French";
let defaultValue = "English";
let language = userPref ?? systemPref ?? defaultValue;
console.log(language); // "French"

?? vs. || : The Critical Difference

This is one of the most important distinctions in modern JavaScript. Both ?? and || can provide default values, but they treat different things as "missing."

The Core Difference

Valuevalue || "default"value ?? "default"
null"default""default"
undefined"default""default"
0"default" ← problem!0 ← preserved!
"""default" ← problem!"" ← preserved!
false"default" ← problem!false ← preserved!
NaN"default" ← problem!NaN ← preserved!
"hello""hello""hello"
424242
  • || returns the right side for any falsy value (null, undefined, 0, "", false, NaN)
  • ?? returns the right side only for null and undefined

When This Matters

// Volume control: 0 is a valid volume (mute)
let userVolume = 0;
let volume;

volume = userVolume || 50;
console.log(volume); // 50 (WRONG! User wanted mute (0), got 50)

volume = userVolume ?? 50;
console.log(volume); // 0 (CORRECT! 0 is preserved)
// Display name: empty string might be intentional
let displayName = "";
let name;

name = displayName || "Anonymous";
console.log(name); // "Anonymous" (WRONG if user intentionally cleared their name)

name = displayName ?? "Anonymous";
console.log(name); // "" (preserved, which might be what you want)
// Feature flags: false is a deliberate choice
let darkMode = false;
let isDark;

isDark = darkMode || true;
console.log(isDark); // true (WRONG! User explicitly chose light mode)

isDark = darkMode ?? true;
console.log(isDark); // false (CORRECT! false is preserved)
// Pagination: page 0 should be valid
let currentPage = 0;
let page;

page = currentPage || 1;
console.log(page); // 1 (WRONG! Should be page 0)

page = currentPage ?? 1;
console.log(page); // 0 (CORRECT!)

The Decision Rule

Do I want to replace ONLY null/undefined?
→ Use ??

Do I want to replace any falsy value (null, undefined, 0, "", false, NaN)?
→ Use ||

In practice, ?? is almost always the correct choice for default values. Use || only when you deliberately want to treat 0, "", and false as "missing."

tip

Modern best practice: Default to ?? for providing fallback values. Only use || when you have a specific reason to treat 0, "", or false as values that should trigger the fallback.

Nullish Coalescing Assignment ??=

The nullish coalescing assignment operator ??= assigns a value to a variable only if that variable is currently null or undefined.

let a = null;
a ??= 10;
console.log(a); // 10 (was null, so assigned)

let b = 0;
b ??= 10;
console.log(b); // 0 (was NOT null/undefined, so NOT assigned)

let c = "hello";
c ??= "world";
console.log(c); // "hello" (was NOT null/undefined, so NOT assigned)

let d = undefined;
d ??= 42;
console.log(d); // 42 (was undefined, so assigned)

Practical Usage

// Setting default values on an options object
function initConfig(config) {
config.theme ??= "light";
config.language ??= "en";
config.fontSize ??= 14;
config.notifications ??= true;
return config;
}

let userConfig = { theme: "dark", fontSize: 0 };
let result = initConfig(userConfig);
console.log(result);
// { theme: "dark", language: "en", fontSize: 0, notifications: true }
// theme: preserved ("dark")
// language: added ("en") (was undefined)
// fontSize: preserved (0) (0 is not null/undefined!)
// notifications: added (true) (was undefined)

Logical Assignment Operators

ES2021 introduced three logical assignment operators that combine logical operations with assignment.

OR Assignment: ||=

Assigns the right-hand value only if the left-hand variable is falsy:

let a = 0;
a ||= 10;
console.log(a); // 10 (0 is falsy, so assigned)

let b = "hello";
b ||= "world";
console.log(b); // "hello" ("hello" is truthy, so NOT assigned)

let c = null;
c ||= "default";
console.log(c); // "default" (null is falsy, so assigned)

x ||= y is equivalent to x || (x = y), not x = x || y. The difference is subtle: the assignment only happens if x is falsy.

AND Assignment: &&=

Assigns the right-hand value only if the left-hand variable is truthy:

let a = 1;
a &&= 10;
console.log(a); // 10 (1 is truthy, so assigned)

let b = 0;
b &&= 10;
console.log(b); // 0 (0 is falsy, so NOT assigned)

let c = "hello";
c &&= c.toUpperCase();
console.log(c); // "HELLO" ("hello" is truthy, so assigned)

Nullish Coalescing Assignment: ??=

Assigns only if the left-hand variable is null or undefined (covered above).

Comparison Table

OperatorAssigns WhenPreserves
x ||= yx is falsyTruthy values
x &&= yx is truthyFalsy values
x ??= yx is null/undefinedEverything except null/undefined

Practical Examples

// ||= for ensuring a value exists (treating falsy as empty)
let username = "";
username ||= "Guest";
console.log(username); // "Guest"
// &&= for transforming existing values
let input = " hello ";
input &&= input.trim();
console.log(input); // "hello"
let emptyInput = "";
emptyInput &&= emptyInput.trim();
console.log(emptyInput); // "" (not transformed because "" is falsy)
// ??= for initializing missing properties
let settings = {};
settings.volume ??= 50;
settings.theme ??= "light";
console.log(settings); // { volume: 50, theme: "light" }
// Adding to an existing object without overwriting
let settings = { volume: 0, theme: "dark" };
settings.volume ??= 50;
settings.theme ??= "light";
settings.language ??= "en";
console.log(settings); // { volume: 0, theme: "dark", language: "en" }

Operator Precedence with Logical Operators

When multiple logical operators appear in the same expression, precedence determines the evaluation order.

Precedence Order (Highest to Lowest)

PrecedenceOperatorName
Highest!NOT
&&AND
||OR
Lowest??Nullish coalescing

How This Affects Evaluation

// ! is evaluated first, then &&, then ||
console.log(!false && true || false);
// Step 1: !false → true
// Step 2: true && true → true
// Step 3: true || false → true
// Result: true

// && before ||
console.log(true || false && false);
// Step 1: false && false → false (higher precedence)
// Step 2: true || false → true
// Result: true

// Without precedence knowledge, you might read it as:
// (true || false) && false → true && false → false ← WRONG!

Use Parentheses for Clarity

Even if you know the precedence rules, parentheses make your code easier to read:

// Ambiguous without precedence knowledge
if (isAdmin || isEditor && hasPermission || isSuperUser) { }

// Clear with parentheses
if (isAdmin || (isEditor && hasPermission) || isSuperUser) { }

// Different meaning with different grouping
if ((isAdmin || isEditor) && (hasPermission || isSuperUser)) { }

The ?? Restriction

JavaScript forbids mixing ?? with || or && without parentheses:

// SyntaxError: Unexpected token '??'
let result = true || undefined ?? "default";

// SyntaxError: Unexpected token '??'
let result = true && undefined ?? "default";

// You MUST use parentheses to clarify intent
let result = (true || undefined) ?? "default"; // "default"
let result = true || (undefined ?? "default"); // true

This restriction exists because the interaction between ?? and ||/&& is inherently confusing. Parentheses force you to be explicit about what you mean.

Common Mistake: Using || for Default Values When 0 or "" Are Valid

This is the single most common logical operator bug in JavaScript. It is so prevalent that the ?? operator was created specifically to solve it.

The Problem in Detail

// A function that configures animation speed
function animate(element, options) {
let duration = options.duration || 300; // Default: 300ms
let delay = options.delay || 0; // Default: 0ms
let opacity = options.opacity || 1; // Default: full opacity

console.log(`Duration: ${duration}, Delay: ${delay}, Opacity: ${opacity}`);
}

// User wants: instant animation (0ms), no delay (0ms), invisible (opacity 0)
animate(element, { duration: 0, delay: 0, opacity: 0 });
// Output: "Duration: 300, Delay: 0, Opacity: 1"
// WRONG! All the user's zeros were replaced with defaults!

The developer intended 0 to be a valid value, but || treats 0 as falsy and replaces it with the default.

The Fix: Use ??

function animate(element, options) {
let duration = options.duration ?? 300;
let delay = options.delay ?? 0;
let opacity = options.opacity ?? 1;

console.log(`Duration: ${duration}, Delay: ${delay}, Opacity: ${opacity}`);
}

animate(element, { duration: 0, delay: 0, opacity: 0 });
// Output: "Duration: 0, Delay: 0, Opacity: 0"
// CORRECT! Zeros are preserved because ?? only replaces null/undefined

More Real-World Examples

// BAD: || with a count that could be 0
function displayResults(count) {
let displayCount = count || "No results";
console.log(displayCount);
}
displayResults(0); // "No results" (WRONG! 0 results IS a valid count)
displayResults(null); // "No results" (correct)
displayResults(5); // 5 (correct)

// GOOD: ?? preserves 0
function displayResults(count) {
let displayCount = count ?? "No results";
console.log(displayCount);
}
displayResults(0); // 0 (correct!)
displayResults(null); // "No results" (correct)
displayResults(5); // 5 (correct)
// BAD: || with a string that could be empty
function setNickname(nickname) {
let name = nickname || "Player";
return name;
}
setNickname(""); // "Player" (WRONG if user intentionally wants no nickname)
setNickname(null); // "Player" (correct)

// GOOD: ?? preserves empty string
function setNickname(nickname) {
let name = nickname ?? "Player";
return name;
}
setNickname(""); // "" (preserved)
setNickname(null); // "Player" (correct)
// BAD: || with boolean config
function setupNotifications(enabled) {
let isEnabled = enabled || true;
return isEnabled;
}
setupNotifications(false); // true (WRONG! User explicitly disabled!)
setupNotifications(null); // true (correct, use default)

// GOOD: ?? preserves false
function setupNotifications(enabled) {
let isEnabled = enabled ?? true;
return isEnabled;
}
setupNotifications(false); // false (correct!)
setupNotifications(null); // true (correct)

Quick Decision Guide

Is the variable's value potentially 0, "", or false as a VALID value?
├── Yes → Use ??
└── No, those are "empty" values I want to replace → Use ||

The Same Bug with ||= vs ??=

let config = { retries: 0, verbose: false, prefix: "" };

// BAD: ||= replaces valid falsy values
config.retries ||= 3; // 3 (WRONG! 0 was intentional)
config.verbose ||= true; // true (WRONG! false was intentional)
config.prefix ||= ">"; // ">" (WRONG! "" was intentional)

// GOOD: ??= only replaces null/undefined
config.retries ??= 3; // 0 (preserved)
config.verbose ??= true; // false (preserved)
config.prefix ??= ">"; // "" (preserved)

Summary

JavaScript's logical operators are more powerful than simple boolean logic. Here is what you need to remember:

  • || (OR) returns the first truthy value or the last value if all are falsy. It does not necessarily return a boolean.
  • && (AND) returns the first falsy value or the last value if all are truthy. Like ||, it returns actual values.
  • ! (NOT) always returns a boolean. It converts its operand to boolean and inverts it. !! is a shorthand for Boolean().
  • All three operators use short-circuit evaluation, meaning they stop evaluating as soon as the result is determined. Remaining operands are not evaluated.
  • ?? (Nullish Coalescing) returns the right-hand side only when the left-hand side is null or undefined. It preserves 0, "", false, and NaN.
  • ?? vs ||: Use ?? when 0, "", or false are valid values that should not trigger the fallback. Use || only when you want to replace all falsy values.
  • Logical assignment operators (||=, &&=, ??=) combine logical checks with assignment. Use ??= for initializing missing properties without overwriting existing valid values.
  • Precedence: ! is highest, then &&, then ||, then ??. You cannot mix ?? with || or && without parentheses.
  • The most common mistake is using || for default values when 0 or "" are valid inputs. The ?? operator was created specifically to solve this problem.

With logical operators mastered, you are ready to explore loops, where you will use these operators to build conditions that control how many times your code repeats.