In this article, you’ll learn to handle JavaScript errors and find out why seeing an error isn’t the worst case scenario.

If something can go wrong, it will.

— Murphy’s Law

Why errors exist

I’ll tell you a story. It has nothing to do with programming or JavaScript. But this story will help you understand the benefits of mistakes. And especially - why mistakes should not be hidden, but problems should be reported as early as possible.

Once upon a time, there was a girl named Mary. She was about to turn 5 years old. The parents wanted to surprise Mary and ordered a big pink cake. Mary dreamed about how she would blow out 5 candles and make her most cherished wish.

Mom ordered a cake from the best pastry shop in town. She made an order in advance. The pastry chef said that everything will be fine and the fairy will bring the cake in the midst of the holiday.

But, as time goes on, there is still no cake. Parents are trying to call to clarify the delivery time, but there is no answer.

Then there is a knock at the door. Mary runs to open it. She sees a wonderful fairy with pink wings. She has a huge box in her hands.

Everyone is in a wonderful mood, everyone is happy and looking forward to the culmination of the holiday. They open the box, and it’s empty.

Mary is crying, her parents are in shock, and the fairy does not understand anything. Nothing can be fixed. The end.

What’s happened

It was an ordinary day at the pastry shop. Order # 735443, which was placed by Mary’s mother, was not something special. Everything was done according to plan.

To begin with, consider the algorithm for the operation of the pastry shop:

  1. The pastry shop accepts the order
  2. The pastry shop transfers the order to the bakery
  3. The bakery fulfills the order
  4. The bakery passes the order back to the pastry shop
  5. The pastry shop packs the order
  6. The pastry shop transfers the order to the delivery service
  7. Delivery service delivers the order

An error can occur at any stage. Imagine a situation where everyone wants “the best” and instead of they prefer to remain silent about the problem.

It is possible that even at the first stage, the pastry shop did not receive enough information to fulfill the order. There could be communication problems and only the delivery date and address were in the order details.

Let’s see what happens next:

  1. The pastry shop accepts incomplete order
  2. The pastry shop sends incomplete order to the bakery
  3. The bakery is fulfilling an incomplete order (cool, nothing to do!)
  4. The bakery sends the incomplete order back to the pastry shop (and they don’t notice anything strange, since before there were no problems with incomplete order)
  5. The pastry shop packages incomplete order (all is well, the packaging is in stock)
  6. The pastry shop transfers the incomplete order to the delivery service
  7. Delivery service delivers incomplete order

Checks can be added to the pastry shop algorithm. For instance:

  • At the stage of accepting an order, check that the order must include a product, delivery date and address.
  • At the bakery stage, there must be a product in the order. The bakery is no longer interested in delivery date and address.
  • At the stage of delivery, check that the address, delivery date and packed product are in the order. Problems this time with the address and date were not. But who knows what will happen tomorrow!

— It seems to me that it is enough to add all the checks at the first stage. Why repeat yourself?

— Great question. Each stage is a separate function. She is obliged to check the incoming parameters and if there is something with them out of order - report upstairs by throwing an error.

You don’t know how the algorithm will change tomorrow. You don’t know in what order the functions will be called.

The best way to avoid problems is to report the problem in step 1, of course. And if in an example from real life, this seems obvious, then in programming there is a very great temptation to “go further” instead of terminating the program execution if something goes wrong.

By adding checks at each step, we make sure we won’t always report an issue as quickly as we can.

Even if someone hires a stupid manager (rewrites the function of accepting an order), then a competent baker will not accept an order in which there is no information about what exactly he needs to cook.

Similar problems can arise in programming. For example, suppose you write a function that takes a unique user ID and returns an array of his friends.

const getUserFriends = (userId) => {
  if (!userId) {
    return [];
  }
  
  const user = getUser(userId); 
  const userFriends = [];
        
  for (let friendId of user.friends) {
    userFriends.push(getUser(friendId));
  }
  
  return userFriends;
}

You don’t want the program to break, so you check the userId at the very beginning of the function. If it is not there, you return an empty array.

But is it good? Is it correct?

For the time being, you may not even notice the problem in the code. For example, everything works well, and you always have userId as input. This can last for years. But sooner or later other functions will start calling your function, someone else will call them. As a result, when users start to see a white page instead of a list of their friends, it will take a long and tedious task to figure out the reason.

Error Handling in JavaScript

An error in JavaScript is an object of type Error.

const e = new Error();

You can add an error message to the error to provide more details about it.

const e = new Error('user_id is missing');

JavaScript errors have one interesting feature. They can be thrown.

To throw an error, you should use the throw keyword.

if (Math.random() > 0.5) {
  throw new Error('random error');
}

console.log('no error, execution continues');

The code above will throw an error in half the cases, and the program will stop executing.

In this case, you will see an error message on the screen and information about where the error occurred.

Error: random error
    at /home/runner/9ag23k3avya/index.js:2:9
    at Script.runInContext (vm.js:130:18)
    at Object.<anonymous> (/run_dir/interp.js:209:20)
    at Module._compile (internal/modules/cjs/loader.js:999:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
    at Module.load (internal/modules/cjs/loader.js:863:32)
    at Function.Module._load (internal/modules/cjs/loader.js:708:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
    at internal/main/run_main_module.js:17:47

After the keyword throw, you must specify an object of type Error. Otherwise, there will be an error (but hardly the one you expect).

throw 'an error!';
throw new Object;
throw false;

The example above will not work, because after the throw I wrote a string, an object and a boolean value.

As soon as JavaScript notices the throw keyword, it tries to terminate the execution of the program. The only way to stop it and continue program execution is with a try/catch block.

try {
  if (Math.random() > 0.5) { // Math.random() returns a random number from 0 до 1. In half the cases it's greater than 0.5
    throw new Error('random error'); // throw an error
  }

  console.log('no error');  // print this message if there's no error
} catch (e) {
  console.log(e.message);   // print the message about a 'random error' if it was thrown and got caught
}

console.log('execution continues'); // execution continues and this message is printed to the screen in any case

Before the block of code that can throw an error, I put the try keyword and wrapped all the" dangerous code " in curly braces.

Right behind it, I added a catch block and handled the error inside it.

— I see that there are no parentheses after try, but after catch there is (e).

— Yes. It should be so.

In the catch block, we can analyze the error and decide what to do next. To work with the error, you need to somehow name it. Think of it as a parameter to a function.

You may well write something like catch (myError). But then inside the catch block you will work with object myError, not e.

How will the error message be displayed on the screen then?

— I think console.log (myError.message)

— Right. The error message that we indicated when creating it new Error ('random error'), can be obtained in the catch block. It is in the message field. Since you renamed the parenthesized error after catch, then you need to refer to the message field not as e.message, but as myError.message.

The catch block is useful in that it gives you the ability to decide what to do next. In some cases, the program should be continued, while in others it is wiser to terminate.

You may well throw another error from the catch block.

— I do not understand very well what the point is. Maybe you shouldn’t have caught her then?

— Maybe you shouldn’t. But the reasons for mistakes are different. The Error object gives you the opportunity to understand what’s going on, and decide on the further fate of the program.

In addition to try / catch, there is another important block in JavaScript. It is called finally. The finally block is executed regardless of whether an error occurs in the try block.

One of the common scenarios for using a finally block looks like this:

  • a dangerous operation in the try block grabs resources (files, network, memory)
  • resources must be released in any case, regardless of whether the dangerous operation was successful
  • resources are released in the finally block
try {
  dangerousOperation();
} catch (e) {
  handleError(e);
} finally {
  performCleanUp();
}

You can skip the catch block and use try/finally without it.

try {
  dangerousOperation();
} finally {
  performCleanUp();
}

The code in the finally block will still be executed even if an error occurs in the try block.

— Weird. It seemed to me that the point of try was just to catch the error in catch. And then what is the use of it?

— We are not always able to fix a bug in JavaScript.

It happens that we cannot continue the execution of the function in any way. In this case, we are obliged to report this error up to the function that called us.

With a catch block, it will look like this.

try {
  dangerousOperation();
} catch(e) {
  throw e;
} finally {
  performCleanUp();
}

However, if you remove the catch block leaving only try/finally, the behavior will remain the same. The error will be pushed to the next level.