This blog post is part of the series “Learning web development” – which teaches people who have never programmed how to create web apps with JavaScript.
To download the projects, go to the GitHub repository learning-web-dev-code
and follow the instructions there.
I’m interested in feedback! If there is something you don’t understand, please write a comment at the end of this page.
In this chapter, we look at exceptions in JavaScript. They are a way of handling errors. We’ll need them for the next chapter.
A class in JavaScript is similar to a function: We invoke it with arguments and it returns a result. Two things are different:
new
plus the typical function call syntax. The reason for that is historical.In this section, we look at two built-in classes. We can also define our own classes but that is beyond the scope of this series.
Roughly, the following two expressions are equivalent:
new Array('a', 'b', 'c')
['a', 'b', 'c']
However, the former has a few quirks, which is why the array literal is virtually always the better choice.
Similarly, new Object()
is equivalent to {}
.
instanceof
We can use v instanceof C
to check if a value v
is an instance of a class C
:
> [] instanceof Array
true
> 123 instanceof Array
false
> {} instanceof Object
true
> 'abc' instanceof Object
false
Error
Error
is a class that is used for reporting errors in JavaScript. Creating an error looks like this:
new Error('Something went wrong')
Errors have a few interesting properties:
> const err = new Error('Message');
> err.message
'Message'
> err.name
'Error'
create-error.js
Property error.stack
tells us where in the source code an Error
instance was created. That means we can tell where an error happened! Project create-error.js
demonstrates how that works:
const createError = () => {
return new Error('Something went wrong!');
};
const err = createError();
console.log(err.stack);
Running it via Node.js produces the following output (there are more lines, but those are not interesting):
Error: Something went wrong!
at createError (create-error.js:2:10)
at Object.<anonymous> (create-error.js:5:13)
err
was created in line 2 column 10 of the file create-error.js
, during the execution of the function createError()
.Object.<anonymous>
.)The lines starting with at
are called a stack trace. If you’re interested as to why: Function calls are managed via a data structure called a stack. A stack trace shows which function calls were made – it is a snapshot of the so-called function call stack.
“Throwing an exception” roughly means “reporting an error”. We can throw any value, but it makes sense to use instances of Error
because then we get stack traces. In the following code, the throw
statement is used to throw an exception:
const divide = (dividend, divisor) => {
if (divisor === 0) {
throw new Error('Division by zero not supported');
}
return dividend / divisor;
};
Normally, an exception ends the current code fragment. However, we’ll soon learn a way to prevent that.
The root error class is Error
, but it also has so-called subclasses. Their relationship is similar to Mammal (Error
) vs. Human (subclass of Error
): All objects created by subclasses of Error
are still instances of Error
.
These subclasses have two benefits:
The following subclasses exist (source):
AggregateError
represents multiple errors at once.RangeError
indicates a value that is not in the set or range of allowable values.ReferenceError
indicates that an invalid reference value has been detected.SyntaxError
indicates that a parsing error has occurred.TypeError
is used to indicate an unsuccessful operation when none of the other NativeError objects are an appropriate indication of the failure cause.URIError
indicates that one of the global URI handling functions was used in a way that is incompatible with its definition.We can distinguish instances via instanceof
or via .name
:
> const err = new TypeError();
> err instanceof TypeError
true
> err.name
'TypeError'
We can also create our own subclasses of Error
, but that is beyond the scope of this series. “Exploring JavaScript” has a chapter on classes and a section on subclassing Error
.
The try-catch
statement lets us prevent an exception from terminating a program:
try {
throw new Error();
} catch (err) {
// Handle the error here
}
Function calls can be arbitrarily deeply nested:
When an exception is thrown during a deeply nested function call, the exception is passed on to each invoking function until it either reaches a catch
or the top level of the program (which is then terminated). In other words: The following try-catch
catches all exceptions that happen during the execution of the function call someFunc()
.
try {
someFunc();
} catch (err) {
// Handle errors
}
Consider the following code. It reads profiles stored in files into an Array with instances of class Profile
:
function readProfiles(filePaths) {
const profiles = [];
for (const filePath of filePaths) {
try {
const profile = readOneProfile(filePath);
profiles.push(profile);
} catch (err) { // (A)
console.log('Error in: '+filePath, err);
}
}
}
function readOneProfile(filePath) {
const profile = new Profile();
const file = openFile(filePath);
// ··· (Read the data in `file` into `profile`)
return profile;
}
function openFile(filePath) {
if (!fs.existsSync(filePath)) {
throw new Error('Could not find file '+filePath); // (B)
}
// ··· (Open the file whose path is `filePath`)
}
Let’s examine what happens in line B: An error occurred, but the best place to handle the problem is not the current location, it’s line A. There, we can skip the current file and move on to the next one.
Therefore:
throw
statement to indicate that there was a problem.try-catch
statement to handle the problem.When we throw, the following constructs are active:
readProfiles(···)
for (const filePath of filePaths)
try
readOneProfile(···)
openFile(···)
if (!fs.existsSync(filePath))
throw
One by one, throw
exits the nested constructs, until it encounters a try
statement. Execution continues in the catch
clause of that try
statement.
Instead of throwing exceptions, JavaScript often uses error values – e.g.:
Getting a non-existing property returns undefined
– which can be considered an error value in this case:
> const obj = {};
> obj.propDoesNotExist
undefined
Dividing by zero produces the error value Infinity
:
> 10 / 0
Infinity
Trying to convert a string with a non-number to a number returns the error value NaN
:
> 'abc' * 3
NaN
Why doesn’t JavaScript throw exceptions in these cases? JavaScript didn’t support exceptions until ES3. That explains why they are used sparingly by the language and its standard library.
For now, we’ll simply throw exceptions when something goes wrong. Without any catching, they’ll be logged to the console (web browser) or the terminal (Node.js).
However, in more polished programs, we should catch exceptions at the appropriate location and either fix what’s wrong or display a useful error message to the user. Doing that well is not easy – even commercial applications from big companies regularly get it wrong!