Boolean Logic Clarity
Boolean logic looks simple. true, false, &&, ||, !. What could go wrong?
Everything. Boolean expressions are where developers make the most logical errors. Off-by-one errors in loops are famous, but off-by-one errors in boolean logic are just as common and harder to spot.
De Morgan's Laws
These two equivalences are essential:
!(a && b) === !a || !b
!(a || b) === !a && !b
Use whichever form reads more naturally.
Example: Negating a Compound Condition
// Original
if (!(user.isAdmin && user.hasPermission)) {
deny();
}
// Apply De Morgan: reads more naturally
if (!user.isAdmin || !user.hasPermission) {
deny();
}
Example: Simplifying Double Negation
// Confusing
if (!(!hasAccess || isBlocked)) { }
// Apply De Morgan: !(A || B) = !A && !B
// So !(!hasAccess || isBlocked) = hasAccess && !isBlocked
if (hasAccess && !isBlocked) { }
Breaking Complex Booleans
When a boolean expression gets complex, break it into named parts.
Before: Wall of Conditions
if (
order.status === 'confirmed' &&
order.paymentReceived &&
inventory.hasStock(order.items) &&
!order.isHeld &&
(order.shippingAddress.country === 'US' || hasInternationalShipping) &&
order.createdAt > cutoffDate
) {
shipOrder(order);
}
You have to read the entire expression to understand when shipping happens.
After: Named Conditions
const isConfirmedAndPaid = order.status === 'confirmed' && order.paymentReceived;
const hasInventory = inventory.hasStock(order.items);
const isNotHeld = !order.isHeld;
const canShipToDestination = order.shippingAddress.country === 'US' || hasInternationalShipping;
const isWithinShippingWindow = order.createdAt > cutoffDate;
const isReadyToShip =
isConfirmedAndPaid &&
hasInventory &&
isNotHeld &&
canShipToDestination &&
isWithinShippingWindow;
if (isReadyToShip) {
shipOrder(order);
}
Now each condition is documented by its name.
Truth Tables for Debugging
When boolean logic is confusing, write a truth table.
The Problem
// This is buggy. Can you spot why?
const canAccess = (isAdmin || isMember) && !isExpired && !isBanned;
Is this right? Let's check with a truth table:
| isAdmin | isMember | isExpired | isBanned | canAccess | Expected |
|---|---|---|---|---|---|
| true | false | false | false | true ✓ | true |
| false | true | false | false | true ✓ | true |
| true | true | true | false | false ✓ | false |
| false | false | false | false | false ✓ | false |
| true | true | false | true | false ✓ | false |
The logic is correct. But writing the truth table forces you to think through edge cases.
Finding a Bug
// Someone wrote this for "allow if user is premium OR if content is free"
const canView = user.isPremium || content.isFree && !content.isRestricted;
Wait—what's the precedence? && binds tighter than ||, so this is:
user.isPremium || (content.isFree && !content.isRestricted)
Truth table:
| isPremium | isFree | isRestricted | canView | Intended |
|---|---|---|---|---|
| true | false | true | true | true ✓ |
| false | true | false | true | true ✓ |
| false | true | true | false | ??? |
Is it intended that free-but-restricted content is blocked? Maybe. The truth table surfaces the question.
Avoid Nested Ternaries
Nested ternary operators create boolean puzzles:
// DON'T
const access = isAdmin ? 'full' : isMember ? 'limited' : isGuest ? 'read-only' : 'none';
This is hard to parse. Use if-else or a lookup:
// DO
function getAccessLevel(user) {
if (user.isAdmin) return 'full';
if (user.isMember) return 'limited';
if (user.isGuest) return 'read-only';
return 'none';
}
// OR
const ACCESS_LEVELS = {
admin: 'full',
member: 'limited',
guest: 'read-only',
};
const access = ACCESS_LEVELS[user.role] ?? 'none';
Short-Circuit Evaluation
JavaScript's && and || short-circuit. This is useful but can be confusing.
Common Pattern: Default Values
const name = user.name || 'Anonymous';
const config = options.config || {};
But watch out for falsy values:
const count = item.count || 10; // Bug if count is 0!
Use nullish coalescing (??) instead:
const count = item.count ?? 10; // Only defaults if null/undefined
Common Pattern: Conditional Execution
// Only call if user exists
user && user.save();
// Better with optional chaining
user?.save();
When Short-Circuit Is Confusing
// What does this return?
const result = a || b && c;
// Answer: depends on values AND precedence
// && binds tighter: a || (b && c)
When precedence isn't obvious, use parentheses:
const result = a || (b && c); // Intent is clear
Simplification Rules
Some patterns can always be simplified:
| Pattern | Simplifies To |
|---|---|
!!value | Boolean(value) or just use in boolean context |
x === true | x |
x === false | !x |
x ? true : false | Boolean(x) or !!x |
x ? false : true | !x |
x && x | x |
| `x | |
x && true | x |
| `x |
Testing Boolean Logic
Complex boolean logic needs tests for each branch:
describe('isEligibleForDiscount', () => {
test('returns true for premium members with enough orders', () => {
expect(isEligibleForDiscount(premiumUserWithOrders)).toBe(true);
});
test('returns false for standard members', () => {
expect(isEligibleForDiscount(standardUser)).toBe(false);
});
test('returns false for premium members with recent refunds', () => {
expect(isEligibleForDiscount(premiumWithRefund)).toBe(false);
});
// Test each condition explicitly
});
Key insight: Boolean expressions are deceptively complex. Use De Morgan's laws to find clearer forms. Break complex expressions into named parts. When confused, draw a truth table. Test every branch.