Lesson Weekend

Note: This lesson is meant to familiarize you with exception handling separately from APIs. We will demonstrate how to incorporate exception handling with API calls in a future lesson. Take this opportunity to absorb the concepts covered here with the understanding that they will be incorporated later in this section.

In an ideal world, all our code would function perfectly and never have errors. But in the real world, errors are inevitable. By this point, you've learned how to debug applications with breakpoints and the DevTools console. You've also learned to lint and to continuously test your code with Jest. Let's take this knowledge one step further and learn about exception handling.

First, what is an exception? An exception is an unusual problem that arises in your code. An exception should be just that: exceptional. What does that mean? Exceptions should handle unexpected errors in our code, not anticipated errors. When a user enters their password incorrectly, that's an anticipated error. Users often make mistakes, so we shouldn't throw exceptions when they do.

However, let's say we have a complex application that handles credit card payments. What would happen if we had a payBalance() function that accidentally charged our customers twice? That would be a serious and unexpected error.

Similarly, when we make an API call, we expect to get a 200 OK as a response. However, if we were to get a 4-- or 5-- error code (an error code in the four or five hundreds), that would be an exceptional error.

Programmers use exception handling to deal with serious and unexpected anomalies in their code. Exception handling is a feature of programming languages in general, not just JavaScript. In JavaScript, we can handle an exception with a try...catch block, which looks like this:

try {
  //Code to try goes here.
} catch {
  //Log any raised errors.
}

We can wrap any code inside the try block. Then, if that code has errors, control will shift to the catch block, where we can write code to handle these errors.

What do we mean by control? Well, control flow is a term used to explain the order a sequence of code will be evaluated. Conditionals change the flow of control in a block of code depending on whether the conditional evaluates to true or false. Likewise, a try...catch block can change control flow if an error is thrown, moving the control into the catch block.

We can't use a try block by itself; doing so will throw an error. try blocks must always be accompanied by either catch, finally, or both. We won't cover finally in depth, other than the fact that a finally block always runs regardless of whether the try block has errors that are caught. finally blocks are often used for cleanup or freeing up resources.

Let's create a very basic application to demonstrate how to use try...catch blocks. The application asks a user to input a number. If the number is negative, the application will throw and catch an error. Before we start, it's important to note that this is not a situation where we'd use exception handling. After all, we expect users to make mistakes. However, we can use this example to show how exception handling works.

The root directory of our application will have two files: try.html and try.js. Note that we aren't using a development environment for this example - we don't need webpack to demonstrate how try...catch blocks work. We will add try...catch blocks in a development environment in a future lesson.

Here's the HTML:

try.html
<html lang="en-US">
<head>
  <script
src="http://code.jquery.com/jquery-3.5.1.min.js"></script>
  <script type="text/javascript" src="try.js"></script>
  <title>Enter a positive number</title>
</head>
<body>
  <div class="container">
    <h1>Please enter a whole number above 0</h1>
    <label for="number">Enter your number:</label>
    <input id="number" type="text">
    <button class="btn-success" id="submittedNumber">Is your number valid?</button>
    <div id="displayNumber"></div>
  </div>
</body>
</html>

Now let's take a look at the JavaScript code:

try.js
$(document).ready(function() {
  $('#submittedNumber').click(function() {
    const inputtedNumber = parseInt($('#number').val());
    $('#number').val("");

    function checkNumber(number) {
      if (isNaN(number) || number < 0) {
        return new Error("Not a valid number!");
      } else {
        return true;
      }
    }

    try {
      const isNumberValid = checkNumber(inputtedNumber);
      if (isNumberValid instanceof Error) {
        console.error(isNumberValid.message);
        throw RangeError("Not a valid number!");
      } else {
        console.log("Try was successful, so no need to catch!");
        $('#displayNumber').text("This number is valid. You may continue.");
      }
    } catch(error) {
      console.error(`Red alert! We have an error: ${error.message}`);
    }
  });
});

We'll skip the jQuery and jump right into the new concepts. First, we have a checkNumber() function which will check to see if the number is NaN or below 0. If it is, it will return an Error.

An Error is a built-in JavaScript object. There are a number of different types of errors that we could specify; for instance, instead of creating a new Error object, we could create a new RangeError. In fact, a RangeError would make more sense here because it's more specific. The documentation for JavaScript's Error object states that a RangeError represents "an error that occurs when a numeric variable or parameter is outside of its valid range." That's exactly the case here.

We also pass a string value into the Error object: return new Error("Not a valid number!"). We should always pass a value into any Error objects we create. When the error is raised, we can see this value by looking at its message property. Because errors can be very difficult to pinpoint in a larger application, the added detail is essential for debugging.

Next, we have our try...catch block. We start with a conditional: if the result of our checkNumber() function is an instanceof Error, our application will throw an error.

We have a new JavaScript operator here: instanceof. instanceof is specifically used to check the type of a JavaScript object. (It does this by looking at the prototype chain of the object, which is a topic beyond the scope of this lesson.) We can test it out in the console:

let error = new Error;
let error2 = new RangeError;
error instanceof Error;
> true
error2 instanceof Error;
> true

Both of the last two expressions return true. Note that while error2 is a rangeError, it's also an Error as well.

You may have stumbled across console.error before, but if you haven't, it operates in a similar fashion to console.log. The only difference is that the message is outlined in red. (There's also console.warn, which is generally used for notifications about deprecated functionality.)

Inside our console.error message, we log the error.message. If we hadn't passed a string into our Error object before, the message property would be undefined and we'd be depriving ourselves of useful information for debugging.

Next, we throw a RangeError to ensure that control moves to the catch block. throw allows developers to define exceptions in an application. For instance, JavaScript itself doesn't care if we call payBalance() twice, charging our customers double in the process. To actually catch that behavior, we'd need to write and throw a custom exception.

If we wanted, we could throw an error outside of a try...catch block. If we were to do so, the program will terminate. That's not really what we want, however. Instead, our application should be able to handle the error gracefully without terminating (unless it's absolutely necessary to terminate).

Our catch block could handle exceptions in a number of ways. The most obvious (and passive) is to log the error. However, since control has moved to the catch block, we could technically run any code we want, including code that allows us to handle the error gracefully without terminating.

It's very simple to incorporate try...catch blocks. In fact, it's easy enough that it can be very tempting to start using these blocks to handle many errors, including unexceptional ones. However, this is a mistake for a number of reasons. When developers see a try...catch block, they will assume it's for handling serious exceptions. Using try...catch blocks in other cases can be confusing. For instance, why would we use try...catch for user input when validations are used for that exact purpose?

try...catch can also result in a performance hit. While this usually won't be an issue, it's important to consider, particularly when an application has a long and resource-intensive backtrace.

Just as importantly, our code would become both unreadable and very painful to write if we wrapped everything in a try...catch block. Think about going through the scanner at the airport. You only need to do that once, when you're going into the terminal, not every time you go to the bathroom!

While the basics of exception handling are relatively easy, knowing when to use exception handling is a more advanced concept that comes with practice. As you build out classwork projects, consider situations where your application might have serious exceptions. You may find opportunities to practice using them even before we incorporate them in a future lesson.

Lesson 9 of 29
Last updated April 8, 2021