308H.2 - Control Flow and Error Handling

Learning Objectives

By the end of this lesson, learners should be able to:

  • Describe block and scope.
  • Create conditional statements within JavaScript.
  • Implement basic error handling with try...catch.
  • Fix basic errors through browser debugging.

Setup

In order to participate in this lesson's examples and activities, let's look at the browser's developer console.

Open a web browser to a blank tab, and press F12 or Ctrl + Shift + I on Windows or + + I.

Alternatively, you can navigate to the developer tools using the menu bar on:

  • Firefox: ➤ Web Developer ➤ Toggle Tools, or Tools ➤ Web Developer ➤ Toggle Tools
  • Chrome: More tools ➤ Developer tools
  • Safari: Develop ➤ Show Web Inspector. If you can't see the Develop menu, go to Safari ➤ Preferences ➤ Advanced, and check the Show Develop menu in menu bar checkbox.
  • Opera: Developer ➤ Developer tools

You may have also accessed these tools during HTML and CSS lessons by using the "Inspect Element" option in the context menu.

For the purposes of this lesson, we'll be using the "Console" and "Sources" tabs within Chrome.

Note that you can type JavaScript code directly into the console, and it will execute!

  • Try this by typing something simple like let x = 10 + 5 and hitting enter.

    • The console will show "undefined" because the let statement does not return any values, it just declares them.
    • Try typing x and hitting enter to see the value of x.
  • Now type let y = x * 2 and hit enter.

    • Type y and hit enter to see the computed value.
    • Notice how the console remembers the value of x?
  • You can clear your console's memory by refreshing the page.

This gives you a simple way to test small snippets of JavaScript code without the need for a local or external file or development environment.


Code Blocks

A block statement is used to group zero or more statements. The block is delimited by a pair of braces ("curly brackets") and contains a list of zero or more statements and declarations. Beyond organizing code, code blocks have implications on the way the code within them is executed.

Blocks are typically paired with other JavaScript statements, such as conditionals (which we will cover next) or functions (which will be covered in a later lesson).

Blocks also have an effect on the way variables and other declarations can be accessed. This is referred to as "scope," which we will cover next.

Here is an example of a very simple block:

let x = 10;
let y = 5;

{
	let z = x + y;
	console.log(z);
}

You can copy and paste this entire piece of code into the browser development console to quickly test its behavior.


Scope

In general, the concept of scope in computer programming pertains to the accessibility of variables and functions from a given point of the code. In other words, as you write a line of code, what variables and functions do you have access to?

JavaScript has three types of scope:

  • Global scope: The default scope for all code running in script mode.
  • Module scope: The scope for code running in module mode.
  • Function scope: The scope created with a function.

In addition, variables declared with let or const can belong to an additional scope:

  • Block scope: The scope created with a pair of curly braces (a block).

In this lesson, we will discuss Global and Block scope. As you continue learning, identifying the scope behavior of modules and functions will be a simple task.

Why the Different Types of Scope?

There's a concept in programming known as The Principle of Least Access.

The principle is based on the idea that limiting the accessibility of variables (and functions) helps reduce bugs in the code - think of it as a form of "code security."

A practical benefit of having different scope, however, is being able to use the same names for variables in different blocks! If there were only one scope, this wouldn't be possible.


Global Scope

In our browsers, the global scope is represented by the window object. The window object is part of the Browser Object Model (BOM), which we will cover in a later lesson, but it is important to understand some key concepts now.

The window object is at the top of the scope chain, and its properties are available to everything we write.

It is generally bad form for our programs to create variables in the global scope. Doing so risks us overwriting data in use by JS libraries/frameworks or other routines.

Creating lots of global variables is referred to as "polluting the global scope," and we all know that it's not nice to pollute!

If we define a variable (or a function) within the global scope, it becomes a property on the window object. You can see this in action by typing var pollution = 'sucks' in the console, then type window. (don't forget the dot), scroll down and find the pollution we have created - yuck!

Although using var in the global scope results in a global variable being created, interestingly, those created using let and const do not appear as properties on the window object. This is yet another benefit of using let and const; avoid global pollution!


Block Scope

Both let and const define variables that can only be accessed within the code block they are defined in.

A code block is created by using curly braces.

The following code modified from MDN's docs about let demonstrates differences between let and var:

{
  var x = 1;
  {
    var x = 2;  // same variable!
    console.log(x);  // 2
  }
  console.log(x);  // 2
}

{
  let x = 1;
  {
    let x = 2;  // different variable
    console.log(x);  // 2
  }
  console.log(x);  // 1
}

Notice how code blocks differentiate between segments of code, allowing us to organize variables and processes. As demonstrated, you can also nest code blocks within one another. Blocks and scope will become very important concepts as you continue to add to your programming repetoire, so make sure you understand the concepts thoroughly.


Examples of Scope

It is important to note that blocks have access to the set of variables and functions defined within their own scope AND in the outer scopes.

When a line of code accesses a variable or function, JavaScript will traverse up the scope chain until it finds what it is looking for.

Let's revisit our original block example:

let x = 10;
let y = 5;

{
	let z = x + y;
	console.log(z);
}

Here, the block has access to both x and y because they exist in the outer scope (in this case, the global scope).

If we layer our logic a bit more, we can also demonstrate how the scope chain accesses all outer scopes, not just the closest:

let x = 10;

{
	let y = 5;
	{
		let z = x + y;
		console.log(z);
	}
}

If the JS runtime engine gets to the global scope (which is the top of the scope hierarchy) and still can't find what it's looking for, that's when your program ceases due to a ReferenceError. We'll talk more about errors and how to handle them later in this lesson.

Here's an example of a ReferenceError. The console.log() statement does not have access to z in this example, because it is block scoped within a block that console.log is outside of:

let x = 10;

{
	let y = 5;
	{
		let z = x + y;
	}
	console.log(z);
}

Running this code will give us the error: Uncaught ReferenceError: z is not defined.

As we continue into this lesson, you will notice how blocks and scope interact with other statements, but keep in mind that the fundamentals are always the same.


Control Flow and Conditionals

In programming, conditionals perform different operations based on the given test parameters. There are many different types of conditionals, which are one of the most frequently-used tools for building scripts and program logic. Using conditionals in this way allows us to control the order in which JavaScript executes the statements in a script, a concept known as "control flow."

Code is typically run in order from the first line in a file to the last, unless the computer runs across structures that change the control flow, such as conditionals (and loops, which we will cover in the next lesson).

For example, this if...else conditional is used to test two conditions:

if (x == 10) {
  // do this
} else {
  // do this
}

if...else statements are just one type of conditional, but they are the most common. The syntax of an if...else statement is shown above; if the expression within the if portion evaluates to true, it will execute the block of code that follows. If it evaluates to false, it will execute the else portion.

You can also forgo the else entirely, and just test a single condition with if, as follows:

if (x = 10) {
	// do this
}


Notice how the code following the if and else statements is organized in blocks. These blocks are not necessary, but without them the if and else would only apply to a single line of code, rather than an entire block.

For example, the following code is valid:

let x = 5;

if (x > 0)
	console.log('Positive!');
else
	console.log('Negative.');

However, including multiple statements requires blocks. This is not valid:

let x = 5;

if (x > 0)
	console.log('Positive!');
	x++;
else
	console.log('Negative.');
	x--;

Attempting to run this code will result in SyntaxError: Unexpected token 'else'. This is because JavaScript is viewing the code like this:

let x = 5;

if (x > 0) {
	console.log('Positive!');
}

x++;

else {
	console.log('Negative.');
}

x--;

Unlike some other languages, simply indenting lines of code does not create blocks. Since you can't have an else without an attached if, the code runs into an error.

It is recommended to always include code blocks alongside statements that traditionally make use of them, like if...else. This makes code more readable, more maintainable, and more expandable.


"Truthy" and "Falsy"

In JavaScript, data type coercion happens automatically in many cases. Numbers can be directly added to strings, etc. This automatic conversion of one data type to another helps provide context to certain situations without the need for strict comparisons.

One of the most common examples of this is in comparisons and logic statements with conditionals.

A "truthy" value is any value that is considered true when it is encountered in a Boolean context, like within an if statement. All values are truthy unless they are defined as falsy, and the only falsy values are:

  • false
  • 0
  • -0
  • 0n
  • ""
  • null
  • undefined
  • NaN

This means that statements like the following will execute the if block, because they are truthy:

if (42) {}
if ("0") {}
if ("false") {}
if (-42) {}
if (Infinity) {}

This also means, however, that you can use conditionals to check for the simple existance of a variable:

if (myVariable) {
	// if myVariable is undefined, it is treated as falsy
}

// instead of
if (typeof myVariable === "undefined") {}

Exercise

Open a code editor and create a file that will run some JavaScript code. Inside of the file:

  • Declare a variable called num.
  • Write a conditional statement that checks if the num is positive or negative.

Once you have completed the above:

  • Change the value of the variable.
  • Save & open it again.

If you were tasked with setting up the security for a web site that only grants access to users over the age of 18, how would you write a conditional that gives users who are 18 or older access, and logs "Access Denied" for users who do not meet the given condition? Give it a try!


Nested if...else

You can test multiple conditions together by "nesting" if...else statements.

The structure of nested if...else looks like this:

if (true) {
	if (true) {
		// do this
	} else {
		// do this
	}
} else {
	//do this
}

You can also "chain" multiple if...else statements together; we call this else if.

if (x >= 10) {
	//do this
} else if (x <= 10>) {
	//do this
} else {
	//do this
}

Exercise

  • Write a nested if...else statement.
  • Declare a variable called num.
  • Add an if...else statement that checks if num is positive & greater than 100.
  • Add another statement that checks if num is positive but less than 100.
  • Add a final statement to check if num is negative.

Exercise

Write an if..else statement for the following requirements:

  • If a learner gets 90 or higher: console.log("A")
  • If a learner get 80 or above: console.log("B")
  • If a learner get 70 or above: console.log("C")
  • If a learner get 55 or above: console.log("D")
  • Any grade lower than 55: console.log("F")

Switch Statements

Another option for control flow is the switch statement.

The switch statement evaluates an expression, matching the expression's value to a case clause, and executes statements associated with that case, as well as statements in any case that follows the matching case.

For example:

let x = '0';
switch (x) {
   case '1':
       // do something...
       break;
   case '2':
       // do something...
       break;
   default:
       // do something...
       break;
}

A breakdown of the syntax for a switch statement is as follows:

  • The switch keyword signifies the start of a switch statement.
  • A value needs to be provided between the parentheses, and this value can be in the form of a literal or variables.
  • The case clause specifies what value the switch argument can be. Both the switch argument and its case values must match in data type.
  • The break keyword tells JavaScript to get out of the switch statement and not evaluate any other case. Without the break keyword, JavaScript will continue to execute other case clauses.
  • The default keyword is like the else statement. Best practice is to always use the default keyword for case values that are invalid.

The example below simulates a simple calculator that can only perform four different arithmetic operations on two numbers.

let x = 5;
let y = 4;
let operand = "*";
switch (operand) {
   case "+":
       console.log(x + y);
       break;
   case "-":
       console.log(x - y);
       break;
   case "*":
       console.log(x * y);
       break
   case "/":
       console.log(x / y);
       break;
   default:
       console.log("Invalid Operand")
       break;
}

Notice that default is being used for the invalid case. Also, the break keyword is not needed in the default case; it depends on the style of the programmer to place the break keyword there.


Ternary Operators

The conditional (ternary) operator is the only JavaScript operator that takes three operands: a condition followed by a question mark (?), then an expression to execute if the condition evaluates to true, followed by a colon (:), and finally the expression to execute if the condition is false.

(conditional) ? (value_if_true) : (value_if_false)

While this is technically an operator and not an element of control flow, this operator is frequently used as an alternative to an if...else statement.

For example, if we wanted to test whether a value was equal to 10, we could do the following:

let x = 10;
let isTen;

if (x == 10) {
	isTen = true;
} else {
	isTen = false;
}

Or, we could use a simple ternary operator to accomplish the same task in a less verbose manner:

let x = 10;
let isTen = x == 10 ? true : false;

Errors

We have mentioned a few errors that you might encounter, such as ReferenceError or SyntaxError. While these two errors are typically fixed within the source code itself, there are many types of errors that you could encounter that are outside of your control. This is particularly true when dealing with external servers or databases.

You can take a look at the MDN JavaScript error reference to see the errors that JavaScript can throw -- there are a lot of them, but some are much more common than others.

We can also create our own errors! This can be useful for a number of reasons, such as validating data before code execution, or simply moving to a different section of code under certain conditions without the need for many if...else statements.

We create our own errors throught the use of the throw statement. The throw statement stops execution of the current block of code (the statements after the throw will not be executed), and the program will terminate (unless... there's a catch. More on that later).

Here's an example where we throw the string "Error - I don't know what I'm doing."

let x = 10;

if (x < 0) {
	console.log("Negative!");
} else {
	throw "Error - I don't know what I'm doing.";
}

console.log("Does this log?");

Executing this code will give us Uncaught Error - I don't know what I'm doing. and no, "Does this log?" will not log.

Why is that? I thought we said only the current block of code will stop execution?

Yes, but we also said the program will terminate (unless... there's a catch). That's how we deal with uncaught errors, we catch them!


Try and Catch

The try...catch control flow statement helps us handle errors whenever they might appear. You will use these statements a lot when dealing with error-prone processes. While it is nice for us as developers to see errors so that we can debug our code, we don't want errors in production. Does that mean we need to make sure errors never occur? No, we just need to handle them properly.

The try block defines code to try. If an error is thrown within that block (either manually or by JavaScript), it will then execute the code within the catch block. There is also an optional finally block that will always be executed before control flow exits the try...catch construct.

Here's an example of this with our previous code:

let x = 10;

try {
	if (x < 0) {
		console.log("Negative!");
	} else {
		throw "Error - I don't know what I'm doing.";
	}
} catch (error) {
	console.log(error);
}

console.log("Does this log?");

The catch block accepts an argument that can be named whatever you'd like, but the error is passed into the catch block as this name. Here, we named it error, but you'll also commonly see it simply named e or err.

In this example, we simply log the error rather than having it thrown to the user. Running this code shows that this does in fact change the behavior of the program, as "Does this log?" does, in fact, log. Since the exception is caught and handled, the script is allowed to continue.

Most of the time, your error handling structures will be a bit more complex, and ensure that the error is properly processed and accounted for, not simply tossed to the side.

Here's slightly a more complex example:

let x = 10;

try {
	if (x > 0) {
		let isEven = (x % 2 == 0) ? true : false;
	} else if (x <= 0) {
		throw "Error - Value of 0 or below.";
	}

	console.log(isEven);
} catch (err) {
	console.log(err);
} finally {
	console.log(x);
}

What will be logged to the console in this example? Why?


Types of Errors

The most common types of errors are:

  • Syntax Errors: errors within the spelling and grammar of your code that cause the program to stop working partially through (or not run at all). These types of errors are usually simple fixes as long as you know what you're looking for.
  • Logic Errors: errors where the syntax is correct, but the logic behind the code is not what you inteded it to be. The program will run, but it will give incorrect results. These errors are much more difficult to debug, as there is no error thrown that directs you to the source of the error.

We've already encountered a syntax error in one of the previous examples. Let's look at that example again and examine the output in the Chrome devtools console:

let x = 5;

if (x > 0)
	console.log('Positive!');
	x++;
else
	console.log('Negative.');
	x--;

As expected, we see the SyntaxError alongside its message: "Unexpected token 'else'".

There is, however, another item on the same line: "VM697:6". This may be slightly different for you, but the important part is the same: :6 designates the line at which this error was encountered. VM697 is what would typically be the file name, but since we are running this code in the browser console, it is assigning that automatically.

Using this information, we can very quickly identify where syntax errors (and other types of errors) occur in our code. Imagine if we had thousands of lines of code across dozens of files, and we were given an error like "SyntaxError: Unexpected token '}'". We would spend days looking for that extra curly brace.

If we click on the VM697:6 link in the console, it will navigate us to the "Sources" tab and open a file viewer that has the syntax error marked for us. Convenient!

Logic errors are less easy to find, but thankfully this "Sources" tab makes it much easier to do so through a practice known as debugging.


Debugging

As we continue our example with debugging in Chrome, it is important to note that most code editors have options for native debugging and many other features. The process is similar if not identical across platforms. Research what might be available to you in order to increase your coding efficiency!

In order to explore debugging, we need to create a proper file to work through. Create an html file with the following contents:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Debugging Example</title>
</head>
<body>
    <script>
        let x = 10;
        let isEven;

        try {
            if (x > 0) {
                isEven = (x % 2 == 0) ? false : true;
            } else if (x <= 0) {
                throw "Error - Value of 0 or below.";
            }

            console.log(isEven);
        } catch (err) {
            console.log(err);
        } finally {
            console.log(x);
        }
    </script>
</body>
</html>

Here we have introduced a very simple, very easy to find logic error, but we'll use it to demonstrate debugging.

If we open this file in Chrome sources, we see the following:

The key feature of a debugger is breakpoints. By clicking to the left of the line numbers in the file viewer, we can set breakpoints that will automatically pause execution of the script so that we can analyze the program at that moment in time and debug it.

While the code is paused, we can examine the current value of variables, execute commands in the console, and accomplish other tasks that will help us find whatever it is we're looking for. You can add and remove breakpoints, or temporarily disable them by using the "Breakpoints" list on the right of the Sources tab.

Chrome and other tools also allow for conditional breakpoints, which only trigger when a given expression is truthy.

With breakpoints set, we can refresh the page and see what happens. We can step through the program using the navigation buttons near the top right, or by using the shortcut key F9 on windows.


In this case, our logic error is within the line isEven = (x % 2 == 0) ? false : true, which simply sets its true and false values opposite of what they should be.

Another option for setting breakpoints is the debugger statement. The debugger statement acts like a breakpoint within the source code itself, and will pause execution of the program at the location of the statement.

For example, we could set a breakpoint in our script like so:

let x = 10;
let isEven;

try {
	if (x > 0) {
		isEven = (x % 2 == 0) ? false : true;
		debugger;
	} else if (x <= 0) {
		throw "Error - Value of 0 or below.";
	}

	console.log(isEven);
} catch (err) {
	console.log(err);
} finally {
	console.log(x);
}

All of these tools will grow in usefulness as your repetoire expands and your programs become more complicated. Take a look through the other options within the Sources tab and make a mental note of things you may want to explore with it in the future!


Break and Continue

There are three remaining control flow statements to discuss, one of which we have already touched on.

The break statement can be used with switch statements to terminate the statement and transfer program control to the statements that follow. It can also be used within loops to terminate execution of the loop.

Likewise, the continue statement can be used to terminate the current iteration of a loop and move onto the next iteration, skipping any code that remains within the loop's current iteration.

Make sense? It shouldn't, yet. We'll be covering loops and iteration in the next lesson, and discussing how break and continue interact with loops in more detail. For now, it is important to know that these are also control flow statements, and therefore relevant to this lesson.


Return

The final control flow statement is another one that we won't put to practice for a couple of lessons. The return statement is used with functions, which allow us to "package" code into more manageable, reusable parts. The return statement basically says, "hey, you asked us to do something, here's the result of that something" and hands that result back to the remainder of the program.

Again, we'll cover this in more detail when we talk about functions.

Copyright © Per Scholas 2024