Sign in

Libre University uses your GitHub account. Signing in is only needed to sit a final test, so the score is kept on your profile.

Making decisions

Everything written so far runs the same lines in the same order and produces the same answer every time, and a program that cannot vary its behaviour with its input is barely a program at all.

The previous lesson introduced the boolean, the type whose only values are true and false. This one is about where booleans come from, how they combine, and how one turns into a fork in the code. If you have arrived here directly: const and let bind names to values, === will appear throughout as the comparison operator, and console.log prints.

Comparison produces a boolean

The relational operators take two values and produce a boolean: <, >, <=, >=, and for equality === and !==. Nothing exotic happens with numbers. 3 < 4 is true, 10 >= 10 is true, 7 !== 7 is false.

The result is an ordinary value, which is worth dwelling on for a second, because beginners tend to treat a comparison as something that only exists inside an if. It does not. It can be bound to a name, printed, or passed to a function like any number:

const isAdult = age >= 18;

That is often the clearer way to write a decision, because the name says what the test means and the if beneath it then reads as English.

Two traps sit in the relational operators. The first is that they do not chain. 3 > 2 > 1 looks like a claim about three numbers and evaluates to false: 3 > 2 gives true, and then true > 1 coerces true to 1, and 1 > 1 is false. Write x > 2 && x < 5, never 2 < x < 5. The second is that < on strings compares character codes, not meaning. "apple" < "banana" is true as you would hope, but "Zebra" < "apple" is also true, because the code for Z is 90 and the code for a is 97, and every capital sorts before every lowercase. Worse, "10" < "9" is true, because comparison walks the characters and 1 comes before 9. Numbers that arrived as text and were never converted sort into an order nobody expects.

=== and the operator to leave alone

JavaScript has two equality operators, and this is one of the few places where a course should give a flat instruction rather than a balanced account. Use === and !==. Do not use == and !=.

=== is strict equality: it is true when the two values have the same type and the same value, and false otherwise. 5 === 5 is true, 5 === "5" is false, and there is nothing further to learn.

== is loose equality, which coerces before comparing, using a set of rules complicated enough that they are usually presented as a table. The reason to avoid it is not that the rules are hard but that the result is not an equality in the mathematical sense. An equality relation must be transitive: if a=b and b=c then a=c. Loose equality is not:

console.log(0 == "0");    // true
console.log(0 == "");     // true
console.log("" == "0");   // false

A relation that fails transitivity cannot be reasoned about, and any argument you make about your own code using == may simply be invalid. The exception the standard style guides allow is x == null, which is true for exactly null and undefined and nothing else, so it is a compact test for "absent by either route". Even that is clearer written out as x === null || x === undefined.

The NaN case from the previous lesson applies to both: NaN === NaN is false, and NaN == NaN is false too. Use Number.isNaN(x).

if makes a fork

An if statement takes a condition and a block, and runs the block only when the condition is true:

if (score >= 50) {
  console.log("pass");
} else {
  console.log("fail");
}

Chain further cases with else if, and note that the chain is ordered: the first matching branch runs and the rest are skipped, so the order of the tests is part of the meaning.

function grade(score) {
  if (score >= 70) return "distinction";
  if (score >= 50) return "pass";
  return "fail";
}

Write those three tests in the opposite order and every score above 70 gets "pass", because score >= 50 is true for it and matches first. That is the classic ordering bug in a grade table, and it produces no error of any kind: the program runs happily and gives the wrong answer for exactly the inputs that mattered most.

Two mechanical warnings. Always use braces, even for a one-line body, because a body without them covers only the next statement and adding a second line later silently puts it outside the if. And a stray semicolon after the condition, if (x > 0);, terminates the statement immediately, so the block that follows runs unconditionally. Both are the kind of mistake you only make once, but only after it has cost you an afternoon.

Example. A shop gives 20 per cent off orders over £100 and 10 per cent off orders over £50. What does this print for an order of 120, and what is wrong with it?

let discount = 0;
if (total > 50) discount = 0.10;
else if (total > 100) discount = 0.20;

For total = 120 the first test succeeds, so discount becomes 0.10 and the second branch is never reached. The order is wrong: the tests overlap, and in an if / else if chain the narrower condition must come first. Swap them and 120 gets 20 per cent, while 60 falls through to the second test and gets 10 per cent.

Now you. A parcel costs £3 to send under 1 kg, £5 under 5 kg, and £9 otherwise. Write the chain in the correct order, and say what an input of 0.5 gives if the tests are written largest first.

Answer
if (weight < 1) return 3;
if (weight < 5) return 5;
return 9;

Written largest first, if (weight < 5) comes before if (weight < 1), so 0.5 kg matches the 5 kg test and is charged £5 instead of £3. Every parcel under 1 kg is overcharged, and no error is ever reported.

Combining conditions

Three operators build compound conditions. && is and, true only when both sides are true. || is or, true when at least one side is true. ! is not, which flips a boolean. Precedence runs ! first, then &&, then ||, so a || b && c means a || (b && c), and again the advice is to bracket rather than to memorise.

&& and || short circuit, which means the right side is not evaluated at all when the left side already decides the answer. In a && b, if a is false the result is false whatever b is, so b is skipped. This is not an optimisation detail, it is something you rely on constantly:

if (user && user.age >= 18) { ... }

If user is undefined, reading user.age would stop the program. The && guarantees it is never read, because the left side has already settled the question. Order the two tests the other way and the program crashes on exactly the input the test was written to protect against.

There is a further subtlety worth knowing, because it explains code you will read. && and || do not return true or false; they return one of their operands. a || b returns a if a is truthy and otherwise b, which is why count || 10 is a shorthand for a default value. That shorthand has a well-known flaw: 0 || 10 is 10, so a genuine zero gets replaced by the default. The fix is ??, the nullish coalescing operator, which falls back only for null and undefined: 0 ?? 10 is 0, and null ?? 10 is 10.

Truthiness, and the six values that are not

if does not require a boolean. It accepts any value and asks whether it is truthy. The rule is a short blacklist: the falsy values are false, 0, "" the empty string, null, undefined and NaN. Everything else is truthy, including "0", "false", the empty array [] and the empty object {}.

This is convenient and it is a trap. if (name) reads as "if a name was given", and it does the right thing for undefined and for the empty string. But if (count) reads as "if there is a count" and is false when the count is legitimately zero, which is a real quantity and usually the interesting case. The habit worth forming is to write the test you actually mean: if (items.length > 0) rather than if (items.length), and if (name !== "") when that is the question.

The interaction between truthiness and loose equality produces the language's most quoted absurdity: [] is truthy, and [] == false is also true. Both are consequences of rules that are individually defensible, and together they are indefensible. Use ===, test explicitly, and the absurdity never comes up.

Negating a condition without getting it wrong

Sooner or later you have to invert a compound test, and doing it by moving the ! around is where correct conditions go to die. The rule is De Morgan's laws, which are as true in code as in logic:

¬(ab)¬a¬b
¬(ab)¬a¬b

In JavaScript: !(a && b) is the same as !a || !b, and !(a || b) is the same as !a && !b. The and becomes an or, and vice versa, and every term flips. Distributing the ! without swapping the connective is the mistake, and it produces a condition that is wrong for exactly half its inputs.

Take a real one. Access is allowed when a user is logged in and not banned: loggedIn && !banned. Access is denied when that is false, which by De Morgan is !loggedIn || banned. Reading it back, denied when they are not logged in, or when they are banned, which is plainly right. The tempting wrong answer, !loggedIn && !banned, denies access only to logged-out unbanned users, letting banned users straight through.

Example. A form is valid when the name is non-empty and the age is at least 18. Write the condition for invalid, and check it against a case.

Valid is name !== "" && age >= 18. By De Morgan the negation is name === "" || age < 18: invalid when the name is empty, or when the age is under 18. Check it on an empty name with age 40. Valid gives false && true, which is false, so it should be invalid; the negation gives true || false, which is true. Agreed.

Now you. A record is archived when it is older than a year or has been marked deleted: ageDays > 365 || deleted. What is the condition for not archived?

Answer

ageDays <= 365 && !deleted. The || becomes &&, ageDays > 365 becomes ageDays <= 365, and deleted becomes !deleted. Check it on a 400 day old record that is not deleted: archived is true || false, which is true, and the negation is false && true, which is false. Agreed.

The shape of a decision

Two idioms are worth adopting now, because they decide how readable your code is a year later.

The first is the guard clause. Deeply nested if statements are hard to read, because by the time you reach the interesting line you are holding four conditions in your head at once. Handle the exceptional cases first and leave the main path unindented:

function withdraw(account, amount) {
  if (amount <= 0) return "invalid amount";
  if (account.frozen) return "account frozen";
  if (amount > account.balance) return "insufficient funds";
  account.balance = account.balance - amount;
  return "ok";
}

Every rejection is one line, the reader can see all three at a glance, and the actual work sits at the bottom at zero indentation. Written as nested if / else blocks the same logic is four levels deep and the reader has to unwind them to answer the question "when does the money actually move".

The second is the conditional expression, or ternary, which is an if that produces a value rather than choosing a statement:

const label = count === 1 ? "item" : "items";

Use it for exactly this, choosing between two values. Nested ternaries are a known readability disaster and an if chain is better as soon as there are three cases. switch is the other tool for many cases, and it is worth knowing that its branches fall through into the next unless each ends with break, which is a defect of design responsible for a steady trickle of bugs.

Example. Rewrite this with guard clauses.

function send(message, user) {
  if (user) {
    if (user.email) {
      if (message.length > 0) {
        return "sent";
      } else {
        return "empty message";
      }
    } else {
      return "no email";
    }
  } else {
    return "no user";
  }
}

Invert each test and return early. The result is flat, and the order of the guards matters: user must be checked before user.email, or the second check stops the program when user is undefined.

function send(message, user) {
  if (!user) return "no user";
  if (!user.email) return "no email";
  if (message.length === 0) return "empty message";
  return "sent";
}

Now you. A booking is accepted when seats remain, the customer is not blocked, and the date is in the future. Write it with guard clauses returning a reason for each rejection.

Answer
function book(event, customer, date) {
  if (event.seatsLeft <= 0) return "sold out";
  if (customer.blocked) return "customer blocked";
  if (date <= today) return "date has passed";
  return "accepted";
}

Each rejection names itself, and the accepted case is the last line at zero indentation.

One fork is not enough

A program can now take a different path depending on its input, which is a real advance: grade above answers differently for 80 and for 40, and no straight-line program can do that.

The limit is that a fork is taken once. Checking whether one number is prime means trying every divisor below it, and there is no number of if statements that will do that for an unknown input, because you would have to write one per divisor and you do not know how many there are. What is needed is a way to say something once and have the machine do it many times, along with an argument that the repetition ever stops. That is the next lesson.