No matter how good of a programmer you are, it is impossible to prevent errors in your program.And they might not necessarily be your fault, especially when dealing with user input. The data that the user provides might not make sense.
By default, when JavaScript encounters an error, the program terminates. For example, the following code calculates the sum of all items in an array.
1let userInput = [1, 2, 3];
2
3let sum = 0;
4
5userInput.forEach((element) => {
6 sum = sum + element;
7});
8
9console.log(sum);
16
If the user provides an invalid input, such as null
, an error will be returned, and the program will terminate.
1let userInput = null;
2
3let sum = 0;
4
5userInput.forEach((element) => {
6 sum = sum + element;
7});
8
9console.log(sum);
1/Users/. . ./index.js:5
2userInput.forEach((element) => {
3 ^
4
5TypeError: Cannot read properties of null (reading 'forEach')
6 at Object.<anonymous> (/Users/. . ./index.js:5:11)
7 . . .
8
9Node.js v21.6.0
Notice that the program terminates at forEach()
, where the error occurs. The rest of the code is simply ignored.
The try catch block
But this is obviously not the best solution. You don't want the user's false input to crush your entire program. Instead, you could use a try catch
block, which tells JavaScript to try something first, catch the error if that doesn't work, and then move on to something else. For instance:
1let userInput = null;
2let sum;
3
4try {
5 sum = 0;
6 userInput.forEach((element) => {
7 sum = sum + element;
8 });
9
10 console.log(sum);
11} catch (error) {
12 console.log(`Invalid user input. Error Message: ${error.message}`);
13}
14
15console.log("JavaScript will continue to execute this line.");
1Invalid user input. Error Message: Cannot read properties of null (reading 'forEach')
2JavaScript will continue to execute this line.
In this case, JavaScript will first try to execute the try
block. If everything works, it will ignore the catch
block and continue to the following code.
But if an error occurs in the try
block, that error will be caught and assigned to the variable error
(of course, the variable can be any other name), and the catch
block will execute.
After that, instead of terminating the entire program, JavaScript will continue executing the next line.
To prevent further errors, you can also give sum
a proper value in the catch
block.
1let userInput = null;
2let sum;
3
4try {
5 . . .
6} catch (error) {
7 console.log(`Invalid user input. Error Message: ${error.message}`);
8 sum = NaN;
9}
10
11console.log(sum + 10);
1NaN
What is in the error object
You probably have guessed from the example (error.message
) that the error
is an object. When JavaScript encounters an error, it organizes the related information into an object, which is then passed to catch ()
as an argument.
There are three properties in the error object:
name
: The error name, such asReferenceError
,TypeError
, and so on.message
: Detailed explanations about the error.stack
: The call stack that leads to the error.
1let userInput = null;
2let sum;
3
4try {
5 sum = 0;
6 userInput.forEach((element) => {
7 sum = sum + element;
8 });
9
10 console.log(sum);
11} catch (error) {
12 console.log(error.name);
13 console.log(error.message);
14 console.log(error.stack);
15}
1TypeError
2Cannot read properties of null (reading 'forEach')
3TypeError: Cannot read properties of null (reading 'forEach')
4 at Object.<anonymous> (/Users/. . ./index.js:6:13)
5 . . .
Creating custom errors
For a real-world project, you cannot rely on JavaScript to recognize all the errors in the program. As we discussed at the very beginning of this course, JavaScript is a weak-type language, and it will always try to execute the code, even if it doesn't make sense.
In this case, you need to tell JavaScript that there is a mistake by throwing your own custom errors. For example:
1let userInput = [1, 2, "3"];
2let sum;
3
4try {
5 sum = 0;
6 userInput.forEach((element) => {
7 sum = sum + element;
8 });
9
10 console.log(sum);
11} catch (error) {
12 console.log(error.name);
13 console.log(error.message);
14 console.log(error.stack);
15}
133
Notice that there are different data types in the array, but JavaScript will automatically convert them into the same data type instead of throwing an error.
In this case, the default behavior is causing the program to return a wrong value. In the context of this program, this should be considered a mistake, but JavaScript cannot recognize it.
So instead, you can throw your own custom error using the keyword throw
.
1let userInput = [1, 2, "3"];
2let sum;
3
4try {
5 sum = 0;
6 userInput.forEach((element) => {
7 // If element is a number
8 if (typeof element === "number") {
9 sum = sum + element;
10 } else {
11 // If element is not a number, throw error
12 throw new Error(
13 "Wrong data type. userInput should be an array of numbers."
14 );
15 }
16 });
17
18 console.log(sum);
19} catch (error) {
20 console.log(error.name);
21 console.log(error.message);
22 console.log(error.stack);
23}
1Error
2Wrong data type. userInput should be an array of numbers.
3Error: Wrong data type. userInput should be an array of numbers.
4 at /Users/. . ./index.js:10:13
5 at Array.forEach (<anonymous>)
6 at Object.<anonymous> (/Users/. . ./index.js:6:13)
7 at Module._compile (node:internal/modules/cjs/loader:1378:14)
8 . . .
The class Error
will automatically create an error object. And JavaScript will be able to catch this error with the try catch
block. You do not have to specify the stack trace, as they will be automatically included. Other similar classes include SyntaxError
, ReferenceError
, TypeError
, and so on.
Extending the Error class
In practice, you might encounter situations where the built-in error classes are insufficient to describe the error. For example, in a web application, you might need a DatabaseError
or HttpError
. If you are using some third-party services, you'll probably need an error class specific to that service, such as StripeError
.
JavaScript allows you to extend based on the default Error
class.
1class DatabaseError extends Error {
2 constructor(message) {
3 super(message);
4 this.name = "DatabaseError";
5 }
6}
7
8try {
9 // Try to connect to database here
10 throw new DatabaseError("Failed to connect to database");
11} catch (error) {
12 console.log(error);
13}
1DatabaseError: Failed to connect to database
2 at Object.<anonymous> (/Users/. . ./index.js:13:9)
3 . . .
The try catch finally block
Optionally, you can create a finally
block that always executes with or without an error.
With an error:
1let userInput = [1, 2, 3];
2let sum;
3
4try {
5 throw new Error("There is an error here.");
6} catch (error) {
7 console.log(error);
8} finally {
9 console.log("This line will always execute with or without an error.");
10}
1Error: There is an error here.
2 at Object.<anonymous> (/Users/. . ./index.js:5:9)
3 . . .
4This line will always execute with or without an error.
Without an error:
1let userInput = [1, 2, 3];
2let sum;
3
4try {
5 // throw new Error("There is an error here.");
6} catch (error) {
7 console.log(error);
8} finally {
9 console.log("This line will always execute with or without an error.");
10}
1This line will always execute with or without an error.
Handling different errors
Sometimes, you might need to deal with different types of errors depending on the situation. For example,
1let userInput = 3;
2
3class DatabaseError extends Error {
4 constructor(message) {
5 super(message);
6 this.name = "DatabaseError";
7 }
8}
9
10try {
11 if (userInput === 1) {
12 throw new SyntaxError("There is an error.");
13 } else if (userInput === 2) {
14 throw new TypeError("There is an error.");
15 } else if (userInput === 3) {
16 throw new DatabaseError("There is an error.");
17 }
18} catch (error) {
19 console.log(error);
20}
As you can see, different errors will be thrown based on different user input, and the catch
block might catch different types of errors. You can create a mechanism where these errors are dealt with in different ways in the catch
block.
1let userInput = 2;
2
3class DatabaseError extends Error {
4 constructor(message) {
5 super(message);
6 this.name = "DatabaseError";
7 }
8}
9
10try {
11 if (userInput === 1) {
12 throw new SyntaxError("There is an error.");
13 } else if (userInput === 2) {
14 throw new TypeError("There is an error.");
15 } else if (userInput === 3) {
16 throw new DatabaseError("There is an error.");
17 }
18} catch (error) {
19 if (error instanceof SyntaxError) {
20 console.log("Dealing with syntax error.");
21 } else if (error instanceof TypeError) {
22 console.log("Dealing with type error.");
23 } else if (error instanceof DatabaseError) {
24 console.log("Dealing with database error.");
25 }
26}
1Dealing with type error.