Logical operators do more than just return true or false—they're powerful tools that leverage short-circuit evaluation to write cleaner, more expressive code. Understanding how &&, ||, and ! really work opens up elegant patterns for default values, conditional execution, and safe property access. Let's master these operators!
Logical Operators Overview
JavaScript has three logical operators:
&&- Logical AND (both conditions must be true)||- Logical OR (at least one condition must be true)!- Logical NOT (inverts boolean value)
These operators work with boolean values, but they can also work with any value, leveraging truthy/falsy evaluation.
The AND Operator (&&)
Basic Boolean Logic
// Basic boolean AND
console.log(true && true); // true
console.log(true && false); // false
console.log(false && true); // false
console.log(false && false); // false
// Only true if BOTH are true
let age = 25;
let hasLicense = true;
if (age >= 18 && hasLicense) {
console.log("Can drive"); // Executes
}
// Multiple conditions
let score = 85;
let attendance = 95;
let homework = true;
if (score >= 80 && attendance >= 90 && homework) {
console.log("Honor roll"); // Executes (all true)
}How && Really Works
The && operator doesn't just return true/false—it returns actual values:
AND (&&) Evaluation Rules:
- Evaluates left to right
- Returns the first falsy value it finds
- If all values are truthy, returns the last value
- Short-circuits (stops) at first falsy value
// && returns values, not just boolean
console.log("hello" && "world"); // "world" (both truthy, return last)
console.log("hello" && 0); // 0 (first falsy found)
console.log(0 && "hello"); // 0 (first falsy found)
console.log(null && "hello"); // null (first falsy found)
console.log("a" && "b" && "c"); // "c" (all truthy, return last)
console.log("a" && 0 && "c"); // 0 (falsy found, stop here)
// With truthy/falsy values
console.log(5 && 10); // 10 (both truthy)
console.log(5 && 0); // 0 (second is falsy)
console.log(0 && 5); // 0 (first is falsy, never checks 5)
console.log("" && "hello"); // "" (first is falsy)
console.log("hello" && ""); // "" (second is falsy)Short-Circuit Evaluation with &&
// If first value is falsy, second is never evaluated
let count = 0;
console.log(count && count.toString());
// Returns 0, never calls toString() (short-circuits)
// Practical: prevent errors
let user = null;
console.log(user && user.name);
// Returns null, never tries to access .name (would error without &&)
// Function calls
function expensive() {
console.log("Expensive function ran");
return "result";
}
console.log(false && expensive());
// Returns false, expensive() never runs (short-circuit)
console.log(true && expensive());
// "Expensive function ran" logged, returns "result"Practical AND Patterns
// Pattern 1: Conditional execution
let isLoggedIn = true;
isLoggedIn && console.log("Welcome back!"); // Executes
// Instead of:
if (isLoggedIn) {
console.log("Welcome back!");
}
// Pattern 2: Safe property access
let user = { name: "John", address: { city: "NYC" } };
let city = user && user.address && user.address.city;
console.log(city); // "NYC"
// Without user
user = null;
city = user && user.address && user.address.city;
console.log(city); // null (safe, no error)
// Pattern 3: Conditional return
function processData(data) {
return data && data.length > 0 && data.map(item => item * 2);
}
console.log(processData([1, 2, 3])); // [2, 4, 6]
console.log(processData(null)); // null
console.log(processData([])); // false (0 is falsy)The OR Operator (||)
Basic Boolean Logic
// Basic boolean OR
console.log(true || true); // true
console.log(true || false); // true
console.log(false || true); // true
console.log(false || false); // false
// True if AT LEAST ONE is true
let isWeekend = true;
let isHoliday = false;
if (isWeekend || isHoliday) {
console.log("Day off"); // Executes
}
// Multiple conditions
let payment = "cash";
if (payment === "cash" || payment === "credit" || payment === "debit") {
console.log("Payment accepted"); // Executes
}How || Really Works
OR (||) Evaluation Rules:
- Evaluates left to right
- Returns the first truthy value it finds
- If all values are falsy, returns the last value
- Short-circuits (stops) at first truthy value
// || returns values, not just boolean
console.log("hello" || "world"); // "hello" (first truthy found)
console.log(0 || "hello"); // "hello" (first is falsy, second is truthy)
console.log("" || "default"); // "default" (first is falsy)
console.log(null || undefined); // undefined (both falsy, return last)
console.log(0 || "" || "value"); // "value" (first two falsy, third truthy)
// With numbers
console.log(0 || 5); // 5 (0 is falsy)
console.log(5 || 10); // 5 (first truthy found)
console.log(0 || false || null); // null (all falsy, return last)
// Short-circuit example
console.log(true || expensive());
// Returns true, expensive() never runsDefault Values with ||
// Most common use: default values
let username = "";
let displayName = username || "Guest";
console.log(displayName); // "Guest"
username = "John";
displayName = username || "Guest";
console.log(displayName); // "John"
// Function parameters
function greet(name) {
name = name || "Guest";
console.log(`Hello, ${name}!`);
}
greet(); // "Hello, Guest!"
greet("Alice"); // "Hello, Alice!"
// Multiple fallbacks
let value = null;
let result = value || localStorage.get("key") || "default";
// Tries value, then localStorage, then "default"
// Configuration with defaults
function createConfig(options) {
options = options || {};
return {
theme: options.theme || "light",
fontSize: options.fontSize || 16,
autoSave: options.autoSave !== false // Different pattern for booleans
};
}Gotcha: || with 0 and ""
// PROBLEM: 0 is falsy
let count = 0;
let displayCount = count || "N/A";
console.log(displayCount); // "N/A" (not what we want!)
// SOLUTION: Check explicitly
displayCount = count !== null && count !== undefined ? count : "N/A";
console.log(displayCount); // 0 (correct!)
// Or use nullish coalescing (modern)
displayCount = count ?? "N/A";
console.log(displayCount); // 0 (correct!)
// PROBLEM: Empty string is falsy
let message = "";
let text = message || "No message";
console.log(text); // "No message" (might not want this)
// If empty string is valid, check explicitly
text = message !== null && message !== undefined ? message : "No message";
console.log(text); // "" (empty string preserved)The NOT Operator (!)
Basic NOT Logic
// NOT inverts boolean
console.log(!true); // false
console.log(!false); // true
// With variables
let isLoggedIn = false;
if (!isLoggedIn) {
console.log("Please log in"); // Executes
}
// Checking for empty/falsy
let username = "";
if (!username) {
console.log("Username required"); // Executes
}
// Negating conditions
let hasErrors = true;
if (!hasErrors) {
console.log("Success");
} else {
console.log("Has errors"); // Executes
}NOT with Truthy/Falsy Values
// NOT converts to boolean first, then inverts
console.log(!"hello"); // false (truthy becomes true, then inverted to false)
console.log(!""); // true (falsy becomes false, then inverted to true)
console.log(!0); // true
console.log(!42); // false
console.log(!null); // true
console.log(!undefined); // true
console.log(![]); // false (arrays are truthy!)
console.log(!{}); // false (objects are truthy!)
// Practical uses
let items = [];
if (!items.length) {
console.log("No items"); // Executes
}
let user = null;
if (!user) {
console.log("No user"); // Executes
}Double NOT (!!) - Convert to Boolean
// !! converts any value to proper boolean
console.log(!!"hello"); // true
console.log(!!""); // false
console.log(!!0); // false
console.log(!!42); // true
console.log(!![]); // true
console.log(!!null); // false
// How it works:
// First ! converts to boolean and inverts
// Second ! inverts back to original truthiness
let value = "test";
let isTruthy = !!value;
console.log(isTruthy); // true
console.log(typeof isTruthy); // "boolean"
// Same as Boolean()
console.log(!!value === Boolean(value)); // true
// Practical: ensure boolean type
function isValid(data) {
return !!data && !!data.name && !!data.email;
}
console.log(isValid({ name: "John", email: "john@test.com" })); // true
console.log(isValid({ name: "John" })); // falseShort-Circuit Evaluation Patterns
Pattern 1: Default Values
// Simple default
let name = username || "Guest";
// Multiple fallbacks
let theme = userTheme || savedTheme || "light";
// With function calls (only runs if needed)
let data = cachedData || fetchFromAPI() || [];
// Object properties
let config = {
port: options.port || 3000,
host: options.host || "localhost",
debug: options.debug || false
};Pattern 2: Conditional Execution
// Execute only if condition is true
isAdmin && deleteUser();
hasPermission && editPost();
isLoggedIn && redirectToDashboard();
// Instead of:
if (isAdmin) deleteUser();
if (hasPermission) editPost();
if (isLoggedIn) redirectToDashboard();
// With callbacks
onSuccess && onSuccess(data);
callback && callback();
// Conditional method calls
user && user.updateProfile();
element && element.focus();Pattern 3: Safe Property Access
// Nested property access
let city = user && user.address && user.address.city;
// Method calls
let result = obj && obj.method && obj.method();
// Array access
let firstItem = array && array.length && array[0];
// Multiple levels
let value = data &&
data.results &&
data.results[0] &&
data.results[0].value;
// Modern alternative: Optional chaining
city = user?.address?.city;
result = obj?.method?.();
firstItem = array?.[0];
value = data?.results?.[0]?.value;Pattern 4: Guard Clauses
function processUser(user) {
// Return early if no user
if (!user) return;
// Return early if no name
if (!user.name) return;
// Main logic
console.log("Processing:", user.name);
}
// Or with OR for error messages
function validate(data) {
if (!data) return "No data provided";
if (!data.email) return "Email required";
if (!data.password) return "Password required";
return null; // Valid
}
// Combining patterns
function getUser(id) {
let user = findUser(id);
return user || null; // Return user or null
}
function getUserName(id) {
let user = findUser(id);
return user && user.name || "Unknown"; // Safe access with default
}Pattern 5: Boolean Flags
// All conditions must be met
let canCheckout = hasItems && hasAddress && hasPayment;
// At least one condition must be met
let canProceed = isAdmin || isOwner || hasPermission;
// None of the conditions should be met
let isEmpty = !hasItems && !hasUsers && !hasData;
// Complex combinations
let canEdit = (isAdmin || isOwner) && !isLocked && hasPermission;
// With functions
let isValid = validateEmail(email) &&
validatePassword(password) &&
termsAccepted;Nullish Coalescing Operator (??)
ES2020 introduced the nullish coalescing operator (??) which only treats null and undefined as falsy, not 0 or "".
// Problem with ||: 0 and "" are treated as falsy
let count = 0;
console.log(count || 10); // 10 (wrong! 0 is valid)
// Solution with ??: only null/undefined are falsy
console.log(count ?? 10); // 0 (correct!)
// More examples
console.log(0 ?? 5); // 0 (0 is not null/undefined)
console.log("" ?? "default"); // "" (empty string is not null/undefined)
console.log(false ?? true); // false (false is not null/undefined)
console.log(null ?? 5); // 5 (null is nullish)
console.log(undefined ?? 5); // 5 (undefined is nullish)
// Practical use
function createUser(options) {
return {
name: options.name ?? "Guest",
age: options.age ?? 0, // 0 is valid age!
score: options.score ?? 0, // 0 is valid score!
active: options.active ?? true
};
}
console.log(createUser({ age: 0, score: 0 }));
// { name: "Guest", age: 0, score: 0, active: true }
// With ||, age and score would be replaced with defaults!When to use ?? vs ||:
- Use
??when 0, false, or "" are valid values - Use
||when you want any falsy value to trigger the default
Optional Chaining (?.)
ES2020 also introduced optional chaining (?.) which replaces long && chains for safe property access.
// Old way with &&
let city = user && user.address && user.address.city;
// New way with optional chaining
let city = user?.address?.city;
// Method calls
let result = obj?.method?.();
// Array access
let firstItem = array?.[0];
// Dynamic properties
let value = obj?.[propertyName];
// Complex example
let data = response?.data?.results?.[0]?.value;
// Combining with ??
let city = user?.address?.city ?? "Unknown";
let name = user?.profile?.name ?? "Guest";
// With function calls
let result = callback?.() ?? defaultValue;
// Practical example
function getUserCity(user) {
return user?.address?.city ?? "No city";
}
console.log(getUserCity(null)); // "No city"
console.log(getUserCity({})); // "No city"
console.log(getUserCity({ address: { city: "NYC" } })); // "NYC"Practical Examples
Example 1: Configuration with Defaults
function createServer(options = {}) {
// Using || for string/number defaults
let config = {
port: options.port || 3000,
host: options.host || "localhost",
protocol: options.protocol || "http",
// Using ?? for boolean/0 values
timeout: options.timeout ?? 30000,
retries: options.retries ?? 3,
verbose: options.verbose ?? false,
// Using && for conditional settings
ssl: options.ssl && {
cert: options.ssl.cert,
key: options.ssl.key
}
};
return config;
}
console.log(createServer({}));
// { port: 3000, host: "localhost", protocol: "http",
// timeout: 30000, retries: 3, verbose: false, ssl: false }
console.log(createServer({ port: 8080, timeout: 0, verbose: true }));
// { port: 8080, host: "localhost", protocol: "http",
// timeout: 0, retries: 3, verbose: true, ssl: false }Example 2: Form Validation with Short-Circuit
function validateForm(data) {
// Check existence with &&
let hasUsername = data && data.username && data.username.trim();
let hasEmail = data && data.email && data.email.includes("@");
let hasPassword = data && data.password && data.password.length >= 8;
// Return first error or null
return !hasUsername && "Username required" ||
!hasEmail && "Valid email required" ||
!hasPassword && "Password must be 8+ characters" ||
null; // Valid
}
console.log(validateForm({}));
// "Username required"
console.log(validateForm({ username: "john", email: "invalid" }));
// "Valid email required"
console.log(validateForm({
username: "john",
email: "john@test.com",
password: "12345678"
}));
// null (valid)Example 3: API Response Handler
function handleAPIResponse(response) {
// Safe property access with optional chaining
let data = response?.data;
let status = response?.status ?? 500;
let message = response?.message ?? "Unknown error";
// Conditional execution with &&
let isSuccess = status >= 200 && status < 300;
isSuccess && console.log("Success:", data);
!isSuccess && console.error("Error:", message);
// Return with defaults
return {
success: isSuccess,
data: data ?? null,
message: isSuccess ? "Success" : message,
items: data?.items ?? [],
total: data?.total ?? 0
};
}
// Test different responses
console.log(handleAPIResponse(null));
// { success: false, data: null, message: "Unknown error", items: [], total: 0 }
console.log(handleAPIResponse({
status: 200,
data: { items: [1,2,3], total: 3 }
}));
// { success: true, data: {...}, message: "Success", items: [1,2,3], total: 3 }Logical Operators Explorer
Experiment with logical operators and short-circuiting
console.log() to see your output in the console above.Key Takeaways
&&returns first falsy value or last value if all truthy||returns first truthy value or last value if all falsy!inverts boolean;!!converts to boolean- Short-circuit evaluation stops as soon as result is determined
- Use
||for default values (but watch for 0 and "") - Use
&&for conditional execution and safe property access ??(nullish coalescing) only treats null/undefined as falsy?.(optional chaining) safely accesses nested properties- Combine operators for powerful patterns:
value ?? "default" - Logical operators work with any values, not just booleans
What's Next?
Congratulations! You've completed the Control Flow section. You now understand how to make decisions in your code with if/else, switch statements, ternary operators, truthy/falsy values, and logical operators with short-circuiting.
In the next section, we'll dive into Loops—learning how to repeat code efficiently with for loops, while loops, and more. Loops are essential for processing arrays, repeating tasks, and handling collections of data!
💪 Practice Challenge:
Before moving on, try creating:
- A function that uses
&&for safe nested property access - A configuration system using
||and??appropriately - A validator that uses short-circuit evaluation to return the first error
- A function that combines optional chaining with nullish coalescing
- A permission checker using complex logical operator combinations
Test Your Understanding
What does this expression return?
'hello' && 'world'