Boolean Logic
Table of Contents
- Two Values, Endless Decisions
- AND, OR and NOT
- Truth Tables
- XOR: Exactly One
- Precedence and Parentheses
- De Morgan's Laws
- Short-Circuit Evaluation
- Untangling a Permission Check
- Recipe: logic.js for mathlab
- Try It Yourself
1. Two Values, Endless Decisions
A boolean has only two possible values: true and false. That
sounds too small to matter, yet every if, every while, every filter
and every permission check in your code boils down to one boolean.
Boolean logic is the math of combining booleans. George Boole wrote
it down in the 1800s, long before computers, and it turned out to be
exactly what a computer chip does: a chip is billions of tiny switches
combining 1s and 0s.
In chapter 3
you combined bits with &, |, ^, and ~. Those operators work on
every bit of a number at the same time. This chapter is about the same
ideas applied to single true/false values with &&, ||, and !.
They are cousins. Once you know one set, you know the other.
If you want to refresh the syntax first, see Operators and Conditions.
2. AND, OR and NOT
There are three basic operations:
| Operation | JavaScript | Meaning |
|---|---|---|
| AND | a && b | true only when both are true |
| OR | a || b | true when at least one is true |
| NOT | !a | flips true to false and back |
In plain words: "can edit" if you are the author and the post is not locked. "Can see" if you are an admin or the owner. "Not" simply reverses the answer.
const isAuthor = true;
const isLocked = false;
console.log(isAuthor && !isLocked); // true
console.log(isAuthor && isLocked); // false
console.log(isAuthor || isLocked); // true
console.log(!isAuthor); // false
Read isAuthor && !isLocked aloud as "is the author and is not locked".
If a condition is hard to read aloud, it is probably too complicated.
3. Truth Tables
With only two values per input, you can list every possible case. That list is called a truth table. It is the most reliable way to check what a condition really does, because it leaves no case out.
Here is AND for two inputs, a and b:
| a | b | a && b |
|---|---|---|
| F | F | F |
| F | T | F |
| T | F | F |
| T | T | T |
And OR:
| a | b | a || b |
|---|---|---|
| F | F | F |
| F | T | T |
| T | F | T |
| T | T | T |
Notice the count of rows: two inputs give 2 × 2 = 4 rows. Three inputs
give 2 × 2 × 2 = 8 rows. In general, n inputs give 2ⁿ rows. (This
is the same doubling you met in chapter 2: each extra input is one more
bit.) Ten inputs would give 1,024 rows, which a computer checks in a
blink, but would exhaust a human. That is why we let code build the
tables, as in the recipe later.
4. XOR: Exactly One
There is a fourth operation people forget: XOR, short for "exclusive
or". It is true when the two inputs are different, meaning exactly
one of them is true.
| a | b | a XOR b |
|---|---|---|
| F | F | F |
| F | T | T |
| T | F | T |
| T | T | F |
Compare it with OR: the only difference is the last row. OR says "one or both"; XOR says "one, but not both".
JavaScript has no ^^ operator. For booleans, !== does the job,
because "the two values are different" is exactly XOR:
const a = true;
const b = false;
console.log(a !== b); // true (XOR)
console.log(a ^ b); // 1 (bitwise XOR also works, but gives a number)
Everyday XOR situations: "pay by cash or card, but not both", "the
toggle changes state each time it is clicked" (isOn = isOn !== clicked),
or "exactly one of the two password fields is empty".
5. Precedence and Parentheses
When one expression mixes operators, JavaScript needs an order, just like
* happens before + in arithmetic. For booleans the order is:
!first,- then
&&, - then
||.
So a || b && c means a || (b && c), not (a || b) && c.
const a = true, b = false, c = false;
console.log(a || b && c); // true -> a || (b && c)
console.log((a || b) && c); // false
Same inputs, different answers. Do not rely on memorizing precedence. Use
parentheses whenever you mix && and ||, even where they are not
strictly required. A reader should never have to run the precedence rules
in their head.
6. De Morgan's Laws
Augustus De Morgan noticed that when you put a ! in front of a
parenthesis, you can push the ! inside, as long as you swap AND with OR:
!(a && b) is the same as !a || !b
!(a || b) is the same as !a && !b
In words: "not (A and B)" means "not A, or not B". "Not (A or B)" means "not A, and not B".
A real sentence shows why it makes sense. Take the rule "you may enter if NOT (under 18 or without a ticket)". Who gets in? Only people who are not under 18 and do have a ticket. The OR flipped to AND.
You can confirm the law with a truth table:
| a | b | !(a && b) | !a || !b |
|---|---|---|---|
| F | F | T | T |
| F | T | T | T |
| T | F | T | T |
| T | T | F | F |
The last two columns match on every row, so the expressions are equivalent. This is how you prove a rewrite is safe without any fancy proof: if every row of the table agrees, the two conditions are the same.
De Morgan's laws matter most when they let you remove a ! that makes
code hard to read:
const age = 20;
const hasTicket = true;
// Hard to read
const blocked1 = !(age >= 18 && hasTicket);
// Same thing, De Morgan applied. Note that !(age >= 18) is age < 18.
const blocked2 = age < 18 || !hasTicket;
console.log(blocked1, blocked2); // false false
Applying the law to comparisons also flips the comparison operator:
| Original | Equivalent |
|---|---|
!(x > 5) | x <= 5 |
!(x >= 5) | x < 5 |
!(x === 5) | x !== 5 |
A common bug is to flip > into < and forget the equal case.
The opposite of "greater than" is "less than or equal to", not "less
than". Truth tables (or tests at the boundary value) catch this.
7. Short-Circuit Evaluation
&& and || do not always evaluate both sides. JavaScript reads left to
right and stops as soon as the answer is certain:
- In
a && b, ifaisfalse, the answer is alreadyfalse, sobis never looked at. - In
a || b, ifaistrue, the answer is alreadytrue, sobis never looked at.
You can prove it with a function that announces itself:
let calls = 0;
const check = () => {
calls++;
return true;
};
false && check(); // check never runs
true || check(); // check never runs
console.log(calls); // 0
This is more than a speed trick. It is a safety tool. Put the cheap or safe test first, and the dangerous one second:
const user = { name: "Dara", profile: null };
// Safe: if profile is null, the right side is skipped
console.log(user.profile && user.profile.city); // null
// Without the guard, this would throw a TypeError:
// user.profile.city
// The modern shortcut for the same idea
console.log(user.profile?.city); // undefined
There is one more twist in JavaScript. && and || do not return
true or false. They return one of the two operands:
console.log(0 || "default"); // "default"
console.log("Dara" || "x"); // "Dara"
console.log(true && "ready"); // "ready"
console.log(null && 1); // null
That is why name || "Anonymous" works as a default value. Just
remember that || treats 0 and "" as "missing", which can be wrong
for a quantity of zero. Use ?? there, as covered in Operators and
Conditions.
Finally, put the cheapest check first. If one condition is a quick variable test and the other asks the database, write the quick one on the left, and the database call is skipped whenever the quick test already decides the answer.
8. Untangling a Permission Check
Here is the problem this chapter has been building toward. The rules for editing a blog post are:
- Admins can always edit.
- Nobody else can edit a locked post.
- The author can edit their own post.
- An editor can edit anyone's post, unless it is archived.
A tired developer wrote this:
function canEditOld(user, post) {
if (
!(user.role !== "admin") ||
(!post.locked &&
(post.authorId === user.id || (user.role === "editor" && !post.archived)))
) {
return true;
}
return false;
}
It works, but nobody can tell. Let's clean it up in four steps.
Step 1: remove double negatives. !(user.role !== "admin") is "not
(not equal)", and two NOTs cancel out. It is simply
user.role === "admin".
Step 2: return the boolean directly. if (condition) return true; return false; is the same as return condition;.
Step 3: split into early returns. An early return exits the function as soon as the answer is known, like a short circuit but for whole steps. Each rule becomes one flat line, in the order you would explain the rules to a colleague:
function canEdit(user, post) {
if (user.role === "admin") return true; // rule 1
if (post.locked) return false; // rule 2
if (post.authorId === user.id) return true; // rule 3
return user.role === "editor" && !post.archived; // rule 4
}
Step 4: prove it did not change. Never trust a refactor by eye. Run both versions on every combination of the inputs that matter (role, locked, archived, author-or-not) and compare:
let same = true;
let cases = 0;
for (const role of ["admin", "editor", "viewer"]) {
for (const locked of [true, false]) {
for (const archived of [true, false]) {
for (const authorId of [1, 2]) {
const user = { id: 1, role };
const post = { authorId, locked, archived };
cases++;
if (canEditOld(user, post) !== canEdit(user, post)) same = false;
}
}
}
}
console.log(same, cases); // true 24
Three roles times two times two times two is 24 cases, and all agree. This is the same idea as a truth table, and it is the heart of safe refactoring: if all the inputs are enumerable, check them all.
Early returns flatten nesting. Compare "if admin, then ... else if ... else ..." with a list of simple rules read top to bottom. The second is easier to read and easier to change when a fifth rule arrives.
9. Recipe: logic.js for mathlab
Now add the logic module to mathlab. Create src/logic.js:
// All true/false combinations for n inputs.
export function boolCombos(n) {
const rows = [];
for (let i = 0; i < 2 ** n; i++) {
const row = [];
for (let bit = n - 1; bit >= 0; bit--) {
row.push(((i >> bit) & 1) === 1);
}
rows.push(row);
}
return rows;
}
// fn takes n booleans; returns [{ inputs: [...], result }, ...]
export function truthTable(fn) {
return boolCombos(fn.length).map((inputs) => ({
inputs,
result: fn(...inputs),
}));
}
// Print a table to the console.
export function printTable(fn) {
for (const { inputs, result } of truthTable(fn)) {
const cells = inputs.map((v) => (v ? "T" : "F")).join(" ");
console.log(`${cells} | ${result ? "T" : "F"}`);
}
}
// Do two functions give the same result for every combination?
export function areEquivalent(f, g) {
if (f.length !== g.length) return false;
return boolCombos(f.length).every((row) => f(...row) === g(...row));
}
// De Morgan's laws, checked by brute force.
export const deMorganAnd = areEquivalent(
(a, b) => !(a && b),
(a, b) => !a || !b,
);
export const deMorganOr = areEquivalent(
(a, b) => !(a || b),
(a, b) => !a && !b,
);
// The cleaned-up permission rules from section 8.
export function canEdit(user, post) {
if (user.role === "admin") return true;
if (post.locked) return false;
if (post.authorId === user.id) return true;
return user.role === "editor" && !post.archived;
}
A few things to notice. fn.length is the number of parameters a function
declares, so truthTable knows how many inputs to try without being told.
i >> bit shifts the bits of i right (from chapter 3) and & 1 picks
the lowest bit, which is how a counter from 0 to 2ⁿ - 1 becomes every
combination of true and false. ...inputs spreads the array into
separate arguments.
Re-export it from src/index.js:
export * from "./logic.js";
Then try it in demo.js:
import {
printTable,
areEquivalent,
deMorganAnd,
canEdit,
} from "./src/logic.js";
printTable((a, b) => a && b);
console.log(deMorganAnd);
console.log(areEquivalent((a, b) => a || b, (a, b) => !(!a && !b)));
console.log(
canEdit({ id: 7, role: "editor" }, { authorId: 7, locked: true, archived: false }),
);
console.log(
canEdit({ id: 1, role: "admin" }, { authorId: 7, locked: true, archived: false }),
);
Run it:
node demo.js
F F | F
F T | F
T F | F
T T | T
true
true
false
true
The true for deMorganAnd is the program confirming De Morgan's law for
you. The next true shows that a || b equals !(!a && !b), which is
De Morgan's second law used backwards. The last two lines show that a
locked post blocks even its own author, but not an admin.
10. Try It Yourself
- Read a table. Call
printTable((a, b, c) => a && (b || c))and count theTresults. Then change the function to(a && b) || cand compare. Which one istruemore often? - XOR from basics. Write
xor(a, b)using only&&,||, and!(hint: "one or the other, but not both"). UseareEquivalentto prove it equals(a, b) => a !== b. - Fix a negation. Rewrite
!(items.length > 0 && !isLoading)without a leading!, applying De Morgan's law. Check your answer withareEquivalentby turning the comparisons into plain boolean inputs. - Add a rule. Extend
canEditso that a post markeddraft: truecan be edited only by its author or an admin, even by an editor. Where in the list of early returns should the new rule go, and why? - Short-circuit proof. Write a function
loud(label, value)that prints the label and returns the value. Evaluateloud("A", false) && loud("B", true)andloud("A", true) || loud("B", false). Which labels print? - Spot the bug. A teammate rewrote
!(age >= 18)asage < 17. For which ages do the two disagree? Use a loop from 0 to 30 to find out.
Boolean logic works on single yes/no answers. Next we look at collections of things instead: Sets and Set Operations shows how AND, OR, and NOT become intersection, union, and difference when the "values" are whole groups of users or tags.
Buy Me a Coffee
Found this tutorial helpful? Consider buying me a coffee to support more free content!