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.
- 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.
- 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 (
- 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
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
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:
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
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
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:
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
Graceful Error Handling
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
Let’s look at an example of a
try..catch..finally statement in action.
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
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
Let’s go back to our
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
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
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”.
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.