Control Flow

Don't push the for loop, it flows by itself.
— Ancient Chinese proverb

If Statements

Quite often, we need to make decisions in our programs. Let's say we want to display a fancy message when a bunch of tasks are completed. This is a decision: If all the tasks are completed, then we want to display a message. More generally: If a condition holds (is true), then we want to do something.

Conveniently, the language keyword that allows us to accomplish this is called if:

const completed = true;
if (completed) {
  console.log('Hooray, you completed all your tasks!');
}

This would print:

Hooray, you completed all your tasks!

The general form of an if statement looks like this:

if (condition) {
  statements;
}

If condition is true, then the statements inside the curly braces will be executed. If condition is false, nothing will happen.

Note that technically it suffices if the condition is truthy or falsy. We will ignore this detail for now and return to it later.

The simplest condition is a boolean variable. However, nothing prevents us from writing more complex conditions. For example, let's say we have a list of uncompleted tasks (conveniently) named tasks. Then we could check that all tasks have been completed by checking whether tasks is empty (i.e. the length of tasks is zero):

if (tasks.length === 0) {
  console.log('Hooray, you completed all your tasks!');
}

Sometimes you need to do something in one case and something else in another case. The (also conveniently named) else keyword allows you to accomplish exactly that:

if (tasks.length === 0) {
  console.log('Hooray, you completed all your tasks!');
} else {
  console.log('You still have some tasks to complete.');
}

The general form of an if...else statement looks like this:

if (condition) {
  statements1;
} else {
  statements2;
}

If condition is true, the statements corresponding to statements1 will be executed (i.e. the statements inside the curly braces after the if). If condition is false, the statements corresponding to statements2 will be executed (i.e. the statements inside the curly braces after the else).

Note that there may be multiple statements between the curly braces. For example, this is totally valid:

if (tasks.length === 0) {
  console.log('Hooray, you completed all your tasks!');
  console.log('Congratulations!');
  console.log('No really, you are amazing!');
} else {
  console.log('You still have some tasks to complete.');
  console.log("Don't despair!");
}

Assuming tasks has a length of 0 this will print:

Hooray, you completed all your tasks!
Congratulations!
No really, you are amazing!

Sometimes you need to handle more than two cases. Since JavaScript was fresh out of keywords at this point, they allowed you to do so using else if:

if (tasks.length === 0) {
  console.log('Hooray, you completed all your tasks!');
} else if (tasks.length === 1) {
  console.log('Only one task left! Go! Go! Go!');
} else {
  console.log('You still have some tasks to complete.');
}

The general form of an if...else if...else statement looks like this:

if (condition1) {
  statements1;
} else if (condition2) {
  statements2;
} /*possibly more else ifs*/ else if (conditionN) {
  statementsN;
} else {
  statementsElse;
}

Here all the conditions will be checked one after another. As soon as a condition is true, the corresponding statements will be executed. If no condition matches, the statements corresponding to statementsElse will be executed.

You can have any number of else if statements. For example, this is valid:

if (tasks.length === 0) {
  console.log('Hooray, you completed all your tasks!');
} else if (tasks.length === 1) {
  console.log('Only one task left! Go! Go! Go!');
} else if (tasks.length === 2) {
  console.log('You have two tasks to do.');
} else if (tasks.length === 3) {
  console.log('There are three tasks left.');
} else {
  console.log('You still have some tasks to complete.');
}

Note that the else block is not required. If it's missing and none of the conditions are true, nothing will happen.

Truthiness and Falsiness

The condition doesn't necessarily have to be a boolean as JavaScript will automatically evaluate non-boolean values as "truthy" or "falsy" in boolean contexts. For example, you could write something like this:

if (1) {
  console.log('1 is truthy');
} else {
  console.log('1 is falsy');
}

This will print 1 is truthy because JavaScript will consider 1 to be true in this context since 1 is a truthy value.

Generally speaking, a truthy value is considered to be true when encountered in a boolean context (like a condition). A falsy value is considered to be false when encountered in a boolean context.

The most important falsy values are false, 0, '' (empty string), null and undefined. Most other values (like 1, [] (empty array), [3], { example: 'hello' } etc) are truthy.

Try to avoid using non-boolean values in boolean contexts as it can lead to surprising behaviour. Nevertheless it's still useful to know about truthiness and falsiness, as it will otherwise trip you up in certain cases.

Ternary operator

The ternary operator takes a condition, an expression to execute if the condition is truthy and an expression to execute if the condition is falsy. It looks like this:

const done = false;
const doneMsg = 'All tasks are done';
const notDoneMsg = 'There are tasks left';
const msg = done ? doneMsg : notDoneMsg;

The general form is:

condition ? expression1 : expression2;

You can think of the ternary operator as a short, compact way to write a conditional expression. The ternary operator evaluates a condition, and if that condition is true (truthy), the result will have the value of the first expression. If the condition is false (falsy), the result will have the value of the second expression.

There is a very common thing beginning programmers do with ternary operators which looks like this:

const finished = tasks.length === 0 ? true : false;

You should stop for a second and think about why this is unnecessary.

That's right—the expression tasks.length === 0 already evaluates to a boolean value. You can just write this instead:

const finished = tasks.length === 0;

Optional chaining

Consider the following task object:

const nextTask = {
  title: 'Read the Next.js book',
  description: 'Read and understand the Next.js book',
  date: {
    day: 8,
    month: 6,
    year: 2022,
  },
};

Let's say we want to access the day of the task. We can do this by writing nextTask.date.day. But what if the day does not have to be present, i.e. is optional? This could happen, for example, because the user didn't enter a date.

This means that the object could look like this:

const nextTask = {
  title: 'Read the Next.js book',
  description: 'Read and understand the Next.js book',
};

Then nextTask.date.day will fail with

Uncaught TypeError: Cannot read properties of undefined (reading 'day')

This makes sense since nextTask.date will result in undefined and you can't access a property on undefined. But let's say we would like to access the day and set it to undefined if the date property is not present. Then we would need to do something like the following:

const day = nextTask.date !== undefined ? nextTask.date.day : undefined;

Here is what this line does: If nextTask.date is defined, then nextTask.date.day is assigned to day. If nextTask.date isn't defined, then undefined is assigned to day.

Alternatively we could make use of && and write:

const day = nextTask.date && nextTask.date.day;

This is correct because of the way the && operator works. If the first expression is false (or falsy) then && doesn't look at the second expression and immediately returns the value of the first expression. If the first expression is true (or truthy) then && returns the second expression.

We can generally consider an object that has a bunch of values that may be absent (i.e. null or undefined). Working with such values will be annoying and only grow more cumbersome with deeper nesting.

To avoid all this JavaScript allows you to do optional chaining. This works by writing ?. instead of . when trying to work on something that may be absent. The above line would then become:

const day = nextTask.date?.day;

Now the result will be undefined instead of a TypeError.

The switch Statement

The switch statement evaluates an expression and then attempts to match the result against a number of case clauses. As soon as a case clause is matched all following statements are executed until a break statement is encountered. If no case matches and a default statement is present, execution will jump to the code after the default statement.

Here is an example:

switch (tasks.length) {
  case 0:
    console.log('Hooray, you completed all your tasks!');
    break;
  case 1:
    console.log('Only one task left! Go! Go! Go!');
    break;
  case 2:
    console.log('You have two tasks to do.');
    break;
  case 3:
    console.log('There are three tasks left.');
    break;
  default:
    console.log('You still have some tasks to complete.');
}

Don't forget the break statements, otherwise all the code after the matched case will be executed, which is rarely what you want.

While Loops

You can use loops to repeat an action multiple times (usually depending on some condition).

The while loop allows you to execute a statement as long as a certain condition is true:

let counter = 0;

while (counter < 3) {
  console.log(counter);
  counter += 1;
}

This will log the following lines to the console:

0
1
2

The general form of the while loop looks like this:

while (condition) {
  statements;
}

The statements inside the curly braces will be executed as long as condition is true (or, rather, truthy).

The do...while loop is similar to the while loop with one subtle difference. The while loop evaluates the condition before executing the statement. The do...while loop on the other hand evaluates the condition after executing the statement.

Consider this example:

let counter = 0;

do {
  console.log(counter);
  counter++;
} while (counter < 3);

This will log the following lines to the console:

0
1
2
3

The general form of the do...while loop looks like this:

do {
  statements;
} while (condition);

Because of the way the do...while loop works the statement(s) inside the loop body will always be executed at least once. The following example will log Hello to the console once despite the condition being false:

do {
  console.log('Hello');
} while (false);

We recommend that you avoid using do...while loops whenever possible. Regular while loops are easier to understand in most cases.

For Loops

The regular for loop consists of three expressions.

The first expression is the initialization expression and typically initializes some kind of counter. The second expression is the condition expression and typically checks for some condition. If the condition is true, the statement(s) in the loop body execute, otherwise the loop terminates. Finally, the third expression (sometimes called afterthought expression) is evaluated at the end of each loop iteration and typically advances the counter.

As usual, a code example says more than a thousand words:

for (let i = 0; i < 3; i++) {
  console.log(i);
}

This will log the following lines to the console:

0
1
2

You can, among other things, use regular for loops to iterate over arrays:

const tasks = ['Task 1', 'Task 2', 'Task 3'];

for (let i = 0; i < tasks.length; i++) {
  console.log(tasks[i]);
}

This will log the following lines to the console:

Task 1
Task 2
Task 3

However, we will soon learn better ways to perform array iteration.

The break and continue Statements

The break statement gives you a tool to prematurely terminate a loop:

let counter = 0;

while (counter < 4) {
  console.log(counter);
  counter += 1;

  if (counter === 2) {
    break;
  }
}

Here we will immediately exit the loop as soon as counter === 2. Therefore, the following lines will be logged to the console:

0
1

The continue statement terminates the rest of the current iteration and continues with the next iteration of a loop:

let counter = 0;

while (counter < 4) {
  counter += 1;
  if (counter === 2) {
    continue;
  }

  console.log(counter);
}

This will log the following lines to the console:

1
3
4

Note that the loop still continues even after counter becomes 2. However, the rest of the iteration is skipped when counter === 2. Therefore, you won't see the 2 logged to the console.