try catch JavaScript – How to handle errors

try catch JavaScript – How to handle errors

In this tutorial, we will learn how to handle errors in JavaScript using try catch.

What is Error Handling?

Let’s understand the behavior of a typical error.

Let’s say we have a function (Function-1) that calls another function (Function-2) which further calls another function (Function-3). Now, if there is an error then, first the execution will stop at Function-3 itself.

Now, there are two ways errors are usually handled:

  1. On Error Continue: In this case, the error will be swallowed. Basically, it will appear to the caller (in the above example Function-2) as if no error occurred.
  2. On Error Propagates: This is the default behavior. In this case, the error will be propagated to the caller i.e Function-2. The caller will then handle the error or propagate it further (in the above example Function-1).

We will see both of these implementations in the following sections.

NOTE: Errors and exceptions are syntactically synonymous in JavaScript.

Error handling is all about gracefully handling the error and making sure that the application does not crash.

For example, if a user tries to access a file that does not exist, we can handle the error and show a message to the user that the file does not exist. You can check it out yourself by visiting codedamn.com/abc. There is no endpoint /abc in the codedamn website. So, when you request that particular endpoint, the codedamn server instead of getting crashed returns a 404 error. This is how error handling is done.

Exception Handling statements in JS

In JS we have two statements related to exceptions/errors are:

  1. throw statement
  2. try...catch statement

throw statement

We use the throw to generate errors. The syntax is:

throw expressionCode language: JavaScript (javascript)

We can throw anything in JS. It can be a string, number, boolean, object, etc. But it is recommended to throw meaningful errors which has a name and a message (an error code is also helpful). This makes error handling easier because we can detect the kind of error and its cause.

In JS, we can use some predefined error objects like ErrorSyntaxErrorTypeError, etc. They have their own constructors and properties. You can check out the MDN documentation for more details.

throw new Error("Something went wrong");
Code language: JavaScript (javascript)

Here we are throwing an Error with the message “Something went wrong”.

When we also create our own error objects. The best practice is to create a class that extends the Error class. That way it would automatically include the stack trace in the exception response. This is very useful for diagnosing issues later down the line.

class MyError extends Error {
  constructor(message) {
    super(message);
    this.name = "MyError";
  }
}

throw new MyError("This is a custom error")Code language: JavaScript (javascript)

Here we are throwing an MyError with the message “This is a custom error”.

This is all about throwing errors in JS. Now let’s see how to handle them.

try...catch statement

Inside the try block we specify the statements to try. While the block executes if any error is generated the block then captures it. The syntax goes like this:

try {
  // statements to try
} catch (e) {
  // statements to handle any exceptions
}Code language: JavaScript (javascript)

One thing to note is that the control will shift immediately to the catch block if an error is generated. The rest of the statements in the try block will not be executed.

try {
  console.log("This is a try block");

  throw new Error("This is an error");

  console.log("This will not be executed");
} catch (e) {
  console.log("This is a catch block");
}Code language: JavaScript (javascript)

If no exception is thrown in the try block, the catch block will not be executed.

try {
  console.log("This is a try block");
  console.log("No error is thrown in the try block");
} catch (e) {
  console.log("Catch block will not be executed");
}Code language: JavaScript (javascript)

We already saw that JS generates error objects which have all the information about the error. We can access that information using the argument of the catch block.

try {
  throw new Error("This is an error");
} catch (err) { 
  console.log(err.name);
  console.log(err.message);
  console.log(err.stack);
}Code language: JavaScript (javascript)

The output will be:

Error
This is an error
Error: This is an error
    at Object.<anonymous>
    ....Code language: Bash (bash)

The Error object has the following properties:

  1. name: The name of the error is determined by the constructor.
  2. message: The error message. This is the message that we pass to the Error constructor.
  3. stack: The stack trace of the error. It shows the function call stack when the error was generated.

There are many other properties in the Error object. You can check them out in the MDN documentation.

finally block

There is another clause that we can add to the try...catch block. As the name suggests it will always be executed. It is used to execute statements that should be executed regardless of whether an exception is thrown or not. The syntax of try...catch...finally is:

try {
  console.log("This is a try block");
  throw new Error("This is an error");
  console.log("This will not be executed");
} catch (e) {
  console.log("This is a catch block");
} finally {
  console.log("This is a finally block");
}Code language: JavaScript (javascript)

The output will be:

This is a try block
This is a catch block
This is a finally blockCode language: Bash (bash)

To better understand the trycontrol and finally blocks, take a look at this flowchart.

Flowchart showing the try…catch…finally syntax

So, JS starts executing the code inside the try block.

  • If error occurs
    • the control will shift to the catch block.
    • then the control will shift to the finally block.
  • If no error occurs
    • it will ignore the catch block
    • the control will shift to the finally block and execute the statements inside it.

Implementations

Let’s try to put together all the knowledge we have so far and implement the example we discussed in the very beginning.

We will have 3 functions:

  • Function-1
  • Function-2
  • Function-3

Function-1 will call Function-2 and Function-2 will call Function-3Function-3 will throw an error. We will handle the error in Function-2 and Function-1 and compare the result.

So the code would look like this:

No error handling

function f1() {
  console.log("Function 1 called...");
  f2();
  console.log("After function 2 was called...");
}

function f2() {
  console.log("Function 2 called...");
  f3();
  console.log("After function 3 was called...");
}

function f3() {
  console.log("Function 3 called...");
  throw new MyError("Error thrown from function 3", 10);
}

f1();Code language: JavaScript (javascript)

The output is:

No error handled
$ node index.js
Function 1 called...
Function 2 called...
Function 3 called...
/home/damner/code/index.js:15
  throw new Error("Error thrown from function 3", 10);
  ^

Error: Error thrown from function 3
    at f3 (/home/damner/code/index.js:15:9)
    at f2 (/home/damner/code/index.js:9:3)
    at f1 (/home/damner/code/index.js:3:3)
    at Object.<anonymous> (/home/damner/code/index.js:18:1)Code language: JavaScript (javascript)

So:

  • Function 1 called Function 2, which called Function 3.
  • When Function 3 threw an error since there was no error handler at any of the callers the error was thrown to the global scope.
  • Also, note none of the console.log were executed after the function calls. This is because execution stops when an error occurs.

This is how the flow looks:

Control flow when the error is not handled

Let’s see how we can handle the error.

Error Handler at Function-2

function f1() {
  console.log("Function 1 called...");
  f2();
  console.log("After function 2 was called...");
}

function f2() {
  console.log("Function 2 called...");
  try {
    f3();
  } catch (err) {
    console.log("Error caught at Function 2");
    console.log("Error Name: ", err.name);
    console.log("Error Message: ", err.message);
    console.log("Error Stack Trace: ", err.stack);
  }
  console.log("After function 3 was called...");
}

function f3() {
  console.log("Function 3 called...");
  throw new Error("Error thrown from function 3", 10);
}

f1();Code language: JavaScript (javascript)

So, here we add a try...catch block at Function 2 and call the f3 function inside it. If an error occurs, the control will shift to the catch block and the error will be handled there.

Let’s look at the output:

Output when error handled at function 2
$ node index.js
Function 1 called...
Function 2 called...
Function 3 called...

Error caught at Function 2
Error Name:  Error
Error Message:  Error thrown from function 3
Error Stack Trace:  Error: Error thrown from function 3
    at f3 (/home/damner/code/index.js:23:9)
    at f2 (/home/damner/code/index.js:10:7)
    at f1 (/home/damner/code/index.js:3:3)
    at Object.<anonymous> (/home/damner/code/index.js:26:1)
    at Module._compile (internal/modules/cjs/loader.js:1085:14)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
    at Module.load (internal/modules/cjs/loader.js:950:32)
    at Function.Module._load (internal/modules/cjs/loader.js:790:12)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:75:12)
    at internal/main/run_main_module.js:17:47

After function 3 was called...  <------ Console Logs after the function call
After function 2 was called...Code language: Bash (bash)

We can see the message “Error caught at Function 2”. It was logged in the catch block. We have logged other information about the error as well like namemessage and stack.

Interestingly, we can see that the console.log statements after the function calls were executed. This is because the error was handled in the catch block and execution continued.

The flow for this particular scenario will be:

Control flow when the error is handled by Function 2

In this case, to Function-1 everything is okay. It doesn’t know that an error occurred in Function-3. All the lines of code after the function call were executed like they would have been executed if there was no error.

Error Handler at Function-1

Sometimes we don’t want to handle the error at the immediate caller. Although this isn’t a good practice, for the sake of understanding, let’s see how we can handle the error at the caller of the caller.

So the new code would look like this:

function f1() {
  console.log("Function 1 called...");
  try {
    f2();
  } catch (err) {
    console.log("Error caught at Function 2");
    console.log("Error Name: ", err.name);
    console.log("Error Message: ", err.message);
    console.log("Error Stack Trace: ", err.stack);
  }
  console.log("After function 2 was called...");
}

function f2() {
  console.log("Function 2 called...");
  f3();
  console.log("After function 3 was called...");
}

function f3() {
  console.log("Function 3 called...");
  throw new Error("Error thrown from function 3", 10);
}

f1();Code language: JavaScript (javascript)

Running this we would see:

Output when error handled at function 1
Function 1 called...
Function 2 called...
Function 3 called...

Error caught at Function 2
Error Name:  Error
Error Message:  Error thrown from function 3
Error Stack Trace:  Error: Error thrown from function 3
    at f3 (/home/damner/code/index.js:22:9)
    at f2 (/home/damner/code/index.js:16:3)
    at f1 (/home/damner/code/index.js:4:5)
    at Object.<anonymous> (/home/damner/code/index.js:25:1)
    at Module._compile (internal/modules/cjs/loader.js:1085:14)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
    at Module.load (internal/modules/cjs/loader.js:950:32)
    at Function.Module._load (internal/modules/cjs/loader.js:790:12)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:75:12)
    at internal/main/run_main_module.js:17:47

After function 2 was called...Code language: Bash (bash)

Interesting thing to note here. The console.log statements after the f3() call in function 2 didn’t get executed. This is because there was no error handler. So, function 2 had to stop execution and propagate the error to the caller.

Whereas the console.log statements after the f2() call in function 1 was executed as we can see in the last line of the output. This is because the error was handled by function 1.

This is what the flow of execution looks like:

Handling Multiple Errors

We can have different types of errors in the try block. We might want to handle them differently. For example, we might want to handle a TypeError differently than a ReferenceError.

Let’s see how we can do that.

function f1() {
  try {
    // try to access a variable that doesn't exist (ReferenceError)
    console.log("Value of a: ", a);
  } catch (err) {
    if (err instanceof TypeError) {
      console.log("TypeError caught at Function 1");
    } else if (err instanceof ReferenceError) {
      console.log("ReferenceError caught at Function 1");
    } else {
      console.log("Error caught at Function 1");
    }
    console.log("Error Name: ", err.name);
    console.log("Error Message: ", err.message);
    console.log("Error Stack Trace: ", err.stack);
  }
}

f1();Code language: JavaScript (javascript)

We can perform checks on what kind of error we are getting. We can use the instanceof operator to check the type of error. We can further use other properties of the error object to perform checks like error code.

The output of the above code will be:

ReferenceError caught at Function 1

Error Name:  ReferenceError
Error Message:  a is not defined
Error Stack Trace:  ReferenceError: a is not defined
    at f1 (/home/damner/code/index.js:4:33)
    at Object.<anonymous> (/home/damner/code/index.js:19:1)
    at Module._compile (internal/modules/cjs/loader.js:1085:14)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
    at Module.load (internal/modules/cjs/loader.js:950:32)
    at Function.Module._load (internal/modules/cjs/loader.js:790:12)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:75:12)
    at internal/main/run_main_module.js:17:47Code language: Bash (bash)

Here we can see that the error was caught in the catch block and the error was of type ReferenceError. So, we logged the message accordingly.

Important Points

try...catch only work for runtime errors

For a try...catch block to work, the error must occur at runtime. If the error occurs at compile time, the try...catch block won’t work. In other words, the JS code has to be syntactically correct for the try...catch block to work.

try {
  let a = ;
} catch (err) {
  console.log("Error Found");
}Code language: JavaScript (javascript)

The above code will throw an error at compile time. So, the try...catch block won’t work.

try...catch doesn’t work for async code

If the error is thrown from a function that is called asynchronously, the try...catch block won’t work. This is because the error is thrown from a different thread. The try...catch block is only for synchronous code.

function f1() {
  try {
    setTimeout(function(){
        throw new Error("Error inside setTimeout");
    }, 1000);
  } catch (err) {
    console.log("Error caught at Function 1");
  }
}

f1();Code language: JavaScript (javascript)

Here we have set a timeout of 1 second. Inside the timeout function, we are throwing an error. So the error won’t be thrown immediately. It will be thrown after 1 second.

By the time the Error is thrown, the try...catch block has already exited. So, the error is not caught.

Conclusion

In this article, we learned about the try...catch block in JavaScript. We saw how we can use it to handle errors in our code. We also learned about the finally clause.

You can read more articles on my blog arnabsen.dev

Sharing is caring

Did you like what Arnab Sen wrote? Thank them for their work by sharing it on social media.

0/10000

No comments so far