Severin Perez

Graceful Error Handling in JavaScript

August 15, 2018

If there’s one thing that every programmer knows for sure, it’s that nothing ever works perfectly, every time, all the time. Errors are a part of daily programming life and learning to deal with them gracefully is an important part of writing good code. Like most fundamental concepts in coding, error-handling is an immense topic full of complexity; however, we can learn a lot just from understanding the basics. To that end, today we’re going to explore JavaScript error types and how to handle them without crashing your program.

Why Errors Happen

Before exploring errors themselves, we should take a moment to consider how and why they occur. Thinking about the origin of errors is a key part of planning good code. In many ways, coding is about expecting the unexpected—if you know that something could happen, and prepare for it, then your code will perform well regardless of whether that thing actually does happen. It would be great if the “happy path” was the only path, but we know that’s not the case, so the prudent thing to do is to think hard about how program execution might diverge from what you intend, and prepare for it.

So, how do programs end up going astray? Well, in lots of ways—too many to count really, but broadly speaking, sources of errors typically fall into one of the following three categories.

  1. Source Code Errors: Sometimes errors lurk in the source code itself. In other words, the programmer (you and me!) has done something wrong (ack!) Such self-inflicted errors occur when a bit of grammar or syntax is incorrect, or a function is being passed the wrong argument, or due to some other arcane mistake. Don’t worry though, we all do this and it’s not an issue so long as you test everything before production.
  2. Edge Case Errors: Another, somewhat more diabolical, source of errors is edge cases. Typically, when you write and test a function you do so with the expectation that the function will receive an input of a certain type and from within a certain set of values. But what happens when the input is technically of the right type but falls perhaps just outside the set of values you were expecting? These are edge cases and depending how your code is written they can cause unexpected errors. A classic example is a function that receives a string as an argument but relies on the string having actual content. If the function receives an empty string ( "" ), which in JavaScript is a falsy value, but isn’t ready for such a value, then it can cause havoc with your control-flow and very well result in an error.
  3. Uncontrolled Input Errors: The final and most common category of errors are those that occur as a result of uncontrolled input. Most programs deal in some fashion with input that originates outside the source code. In web development, this typically means user input. If, for example, a user is submitting a form, what happens if they neglect to fill out one of the fields? Well, you’re going to get an empty string value from that field, which as we have seen can cause trouble. If you don’t know exactly what values are ultimately going to be supplied to your functions, then you need to think ahead of time about what kind of values might lead to errors.

Types of Errors

We know that errors occur for a variety of reasons, but planning for errors without knowing anything about them would be pretty difficult. Thankfully, JavaScript defines a variety of built-in error types, each of which tells you something different about the source of your error. This is incredibly useful when trying to debug a malfunctioning program. At the most general level, JavaScript raises errors in two situations: either an error occurs at compilation; or, an error occurs at runtime. In the former case, your program won’t even run and instead you will get a message that something is wrong. In the latter case, your program will run up to the point where an error-causing function is executed and then crash.

In total, JavaScript defines 8 types of errors, including: the generic Error; TypeError; ReferenceError; SyntaxError; RangeError; InternalError; EvalError; and, URIError. Each of the sub-error types have the generic Error up their prototype chain. You can read about each of these errors in detail in the documentation; however, the ones that occur most commonly are TypeError, ReferenceError, and SyntaxError. Let’s quickly look at each of them:

  • TypeError: Occurs at runtime when a value is not of the expected type. Typically, this means that you are attempting to call a method on a value that does not recognize the supplied method. (For example, trying to call String.prototype.indexOf() on a value of type Number.) Or, it means that you have tried to execute a non-function value as a function.
  • ReferenceError: Occurs at runtime when you attempt to access a variable that has not yet been declared.
  • SyntaxError: Occurs at compilation time (a so-called “early error”) when your source code is syntactically invalid. Usually this is the result of a missing bracket, parens, or something similar.

Let’s look at the same errors in a code snippet:

console.log("My favorite dinasaur is " + theropod);
  // ReferenceError: theropod is not defined

var sauropod = "apatosaurus";
sauropod();
  // TypeError: sauropod is not a function

if (sauropod === "apatosaurus")
  console.log("We have the same favorite sauropod!");
}
  // SyntaxError: Unexpected token }

On line 1 of this snippet we’re getting a ReferenceError because we attempt to access a variable called theropod, which does not yet exist. On line 5 we are getting a TypeError because we are trying to call the variable sauropod (which contains a String value) as a function. And finally, on line 8 we are getting a SyntaxError because there is a missing curly brace in the if block. Note that if you try to run this snippet you will only see one of these errors at a time. First you will see the SyntaxError from line 8 because it occurs at compilation time. If you remove (or comment out) lines 8–10 then you will see the ReferenceError from line 1 (a runtime error). And finally, if you remove / comment out line 1 you will see the TypeError from line 5.

All errors share two common properties, which you can access in the same manner that you would access properties on any other object. These properties are name and message, and they provide you with information about the type of error and what it means. Different browsers and runtime environments have additional property implementations, such as stack, but most often you will be dealing with name and message because those are consistent across all environments. Because these are just properties you can edit them to match your needs. Take for example the following:

var dinoError = new Error("Oh no! A dino error!");
dinoError.name = "DinoError";

console.log("dinoError name: ", dinoError.name);
  // Logs:  dinoError name:  DinoError

console.log("dinoError message: ", dinoError.message);
  // Logs:  dinoError message:  Oh no! A dino error!

Here we have a generic Error object that we have stored in the variable dinoError. We provide the Error constructor function with a custom message, which it then assigns to dinoError.message. We then directly access the dinoError.name property and give it a custom name (by default it would just have been “Error”). In this fashion, we have created an error with custom properties. However, there are some problems with this implementation of custom errors. For starters, it still has Error as its prototype and we therefore cannot differentiate it from generic errors without relying on the name property having been set to something different. Thankfully, JavaScript provides a much more effective way of creating custom errors, which can be very useful when you are designing an interface that expects certain kinds of inputs. More on that in a bit.

Graceful Error Handling

As a general rule, you should aim to write code that prevents errors from being thrown in the first place; however, in cases where you cannot predict inputs and therefore need to guard against errors, JavaScript provides a handy construct known as the try..catch..finally statement. This statement is a series of blocks that look for errors and capture them before they can crash your program. First, code in the try block executes, and if everything goes well then your program exits the block per normal. If, however, an error occurs in the try block, then the error object is immediately passed to the catch block where you can deal with the error gracefully without the whole program collapsing. If you have chosen to include a finally block in the statement (it’s not required), then the code in it will execute either immediately after the try block, or if an error is caught, then immediately after the catch block. Note that the code in your finally block always executes, even if there is a return statement in your try or catch blocks.

Let’s look at an example of a try..catch..finally statement in action.

function cloneDinosaur(name) {
  try {
    var myDinosaur = {
      name: name,
      dangerMessage: name.toUpperCase() + " IS COMING!!!",
    };
    
    return myDinosaur;
  } catch (e) {
    console.log(e.name + ": " + e.message);
    
    return undefined;
  } finally {
    console.log("Dinosaur clone function complete.");
  }
}

var badUserInput = null;
var dino = cloneDinosaur(badUserInput);
  // Logs:  TypeError: Cannot read property 'toUpperCase' of null
  // Logs:  Dinosaur clone function complete.
console.log(dino);
  // Logs:  undefined

var goodUserInput = "Rex";
var rex = cloneDinosaur(goodUserInput);
  // Logs:  Dinosaur clone function complete.
console.log(rex);
  // Logs:  { name: 'Rex', dangerMessage: 'REX IS COMING!!!' }

In this snippet we have a function called cloneDinosaur, which accepts a name parameter. Not surprisingly, the function expects the name parameter to be a value of String type (sorry to those of you want to name your dinosaurs with numbers). Because we can’t guarantee what kind of input the cloneDinosaur function might receive, we have composed a try..catch..finally statement inside, which will catch errors and exit gracefully rather than crashing our program. We can see this statement in action on lines 18–22 when we provide cloneDinosaur with a null value as an argument. On line 5, when the function attempts to use the String.protototype.toUpperCase method on our null value, it throws a TypeError, which is immediately passed into the catch block. Inside the catch block we log a helpful message (by accessing the error’s name and message properties) and then return undefined from the function. Compare this to the code on lines 25–28, when we pass cloneDinosaur a valid string as input. In this case, everything works as expected and the function returns an object containing information about our dinosaur. Note how in both cases, the finally block executed and logged a message indicating that the function was complete.

As we can see, using a try..finally..catch statement is one way of making sure that errors don’t crash your program. The above snippet is a bit naive (it would have been much easier to use a guard clause to validate that the provided value to cloneDinosaur was a string), but it gives you a sense of how error handling works.

Throwing Custom Errors

Catching built-in errors is a useful capability, but what if you need to recognize errors that are specific to your program rather than to JavaScript itself? To do this, you need custom errors, and as we hinted at earlier JavaScript provides a means to do exactly this.

Let’s go back to our cloneDinosaur function and expand on it a bit. Imagine now that we need our users to tell us what type of dinosaur they want to clone in addition to giving us a name to use. What happens if they provide us with a type that our scientists don’t know how to clone? JavaScript won’t recognize such an error on its own but we can define a custom error to detect this problem and deal with it appropriately. Let’s do just that:

function DinoError(message) {
  this.name = "DinoError";
  this.message = message;
}
DinoError.prototype = new Error();

function cloneDinosaur(name, type) {
  var validTypes = ["Apatosaurus", "Dilophosaurus", "Tyrannosaurus Rex", "Stegosaurus"];
  
  try {
    if (validTypes.indexOf(type) === -1) {
      throw new DinoError("We don't know how to clone the dinosaur type: " + type);
    }
    
    var myDinosaur = {
      name: name,
      type: type,
      dangerMessage: name.toUpperCase() + " IS COMING!!!",
    };
    
    return myDinosaur;
  } catch (e) {
    if (e instanceof DinoError) {
      console.log("You experienced a DinoError!");
    } else {
      console.log("You experienced a standard error.");
    }
    console.log(e.name + ": " + e.message);
    
    return undefined;
  } finally {
    console.log("Dinosaur clone function complete.");
  }
}

var dino = cloneDinosaur("Dino", "Brachiosaurus");
  // Logs:  You experienced a DinoError!
  // Logs:  DinoError: We don't know how to clone the dinosaur type: Brachiosaurus
  // Logs:  Dinosaur clone function complete.
console.log(dino);
  // Logs:  undefined
  
var spike = cloneDinosaur(null, "Stegosaurus");
  // Logs:  You experienced a standard error.
  // Logs:  TypeError: Cannot read property 'toUpperCase' of null
  // Logs:  Dinosaur clone function complete.
console.log(spike);
  // Logs:  undefined

var rex = cloneDinosaur("Rex", "Tyrannosaurus Rex");
  // Logs:  Dinosaur clone function complete.
console.log(rex);
  // Logs:  { name: 'Rex', type: 'Tyrannosaurus Rex', dangerMessage: 'REX IS COMING!!!' }

On lines 1–5 of this snippet we define a constructor function called DinoError and then set its prototype to a generic Error object. Our custom DinoError now functions exactly like a regular Error, except that when we use the instanceof keyword on a DinoError we will get back “DinoError” rather than simply “Error”. Now, we can detect our custom error without having to rely on checking its name property.

Now let’s look at our expanded cloneDinosaur function. At the top of the function we define an array of valid dinosaur types that our scientists know how to clone. Then, inside the try block we check to see whether the provided type is included in our validTypes array. If it isn’t one of the valid types, then we use the throw keyword to send out a custom DinoError. The throw keyword can actually be used with anything (not just Error objects), but no matter what it uses, throw sends the provided value racing through the call stack until it encounters a catch block, which can then deal with the value appropriately. In our case, the catch block is set up to check the provided value to see whether it is a DinoError, and if it is it takes one action, and if not, it takes another. In this fashion we can handle both our custom error and standard errors.

We can see the above in action on lines 36–52. First, on line 36 we provide cloneDinosaur with an invalid dinosaur type, and indeed, we get back a message indicating that a DinoError occurred. Then, on line 43, we provide cloneDinosaur with a bad input value for the name parameter. In this case, the function recognizes that a normal error occurred rather than a custom error and it logs a message to that effect. Finally, on line 50 we provide cloneDinosaur with a valid name and type and in return we get a terrifying dinosaur called “Rex”.

TL;DR

Errors are part of everyday life in programming, but thankfully, JavaScript provides the means for you to identify and handle errors gracefully. JavaScript has a variety of built-in error types, the most common of which are ReferenceError, TypeError, and SyntaxError. You can also create custom errors that let you identify program-specific errors and deal with them individually. Typically, an error will cause program execution to cease; however, you can deal with errors gracefully by using a try..catch..finally block that captures errors so that you can handle them without your program crashing.


That’s it for our short introduction to error handling in JavaScript. There is much more to explore on this topic, such as using the stack-trace and re-throwing errors; however, hopefully the above has provided a useful starting point.


Note: This article was originally published on Medium.


You might enjoy...


© Severin Perez, 2021