Error handling, "try..catch"

Error handling, "try..catch"

Error handling, "try..catch"


Even the greatest programmers can have errors in the scripts. Generally, errors can occur as a result of mistakes, unexpected user input, and a thousand other reasons.

But, hopefully, there exists a try..catch syntax construct, allowing to catch errors, so the script can do something more reasonable.

The syntax of try...catch

Two blocks are included in thetry..catch construct: try and catch:

try {
  // code
} catch (err) {
  // error handling
}

The code works in the following order:

  1. The code is executed in try {...}.
  2. catch(err) is ignored if there are no errors. The execution gets to the end of the try and continues, skipping the catch.
  3. In case there is an error, the execution of try is stopped. The control flows to the start of catch(err). So, the err variable (any name can be given to it) will involve an error object that includes details about what happened.

The process is illustrated in the picture below:

Let’s check out examples. In the first example, there is no error:

try {

  console.log('Start test runs'); 

  // no errors here

  console.log('End test runs');

} catch (err) {

  console.log('Catch ignores because there are no errors'); 

}

In the second example, error detection is demonstrated like this:

try {

  console.log('Start test runs'); 

  anyVar; // error, variable isn't defined

  console.log('End of test (never reached)'); 

} catch (err) {

  console.log(`Error has occurred`); 

}

Note that try..catch works only for runnable codes. That is, it works in the valid JavaScript.

If the code is syntactically wrong. For example:

try {

  (((((((((

}

catch (e) {

  console.log("The engine does not understand this code, it is invalid");

}

In the example above, you can see unmatched curly braces.

Generally, the engine of JavaScript first reads the code and then runs it. The errors, occurring in the reading phase are known as “parse-time” errors. They are unrecoverable as the engine doesn’t understand the code.

So, try..catch only catches errors happening in a valid code. Errors like this are known as “runtime errors” or “exceptions”.

Error Object

Once an error happens, JavaScript creates an object, involving the details about it. Then, the object is passed as an argument to catch, like this:

try {
  //...
} catch (err) { //the "error object", could use another word instead of err
}

The error object includes two primary properties for all built-in errors: name (the error name) and message (the textual message about the details of the error).

Other non-standard properties are also available in most environments. One of the most commonly used properties is stack: a string, containing information about the sequence of nested that led to an error. As a rule, developers use it for debugging purposes. Here is an example:

try {

  anyVariable; // error, variable isn't defined

} catch (err) {

  console.log(err.name); // ReferenceError

  console.log(err.message); // anyVariable is not defined

  console.log(err.stack); // ReferenceError: anyVariable is not defined at (...call stack)

  // May also show an error in general

  console.log(err); // ReferenceError: anyVariable is not defined

}

The Usage of “try…catch”

You have already learned from previous chapters that JavaScript supports the JSON.parse(str) method for reading JSON-encoded values.

As a rule, it is used for decoding data, received over the network, either from the server or another source.

It is received and called JSON.parse, as follows:

let json = '{"name":"John", "age": 25}'; // data from the server

let user = JSON.parse(json); // convert text view to js object

//now the user is an object with properties from the string

console.log(user.name); // John

console.log(user.age); // 25

In case json is malformed, JSON.parse creates an error, and the script “dies”. To get out of such a situation, you can use try..catch, like this:

let json = "{ bad json }";

try {

  let user = JSON.parse(json); // an error occurs..

  console.log(user.name); // doesn't work

} catch (e) {

  //the execution jumps here

  console.log("Our apologize, there are errors in the data, we will try to request them again.");

  console.log(e.name);

  console.log(e.message);

}

Throwing errors

Let’s check out a case when json is syntactically correct but doesn’t include a required name property:

let json = '{ "age": 25 }'; // incomplete data

try {

  let user = JSON.parse(json); //no errors

  console.log(user.name); // no name

} catch (e) {

  console.log("doesn't execute");

}

Although JSON.parse runs normally here, but the absence of the name is considered an error.

For unifying the error handling process, you can use the throw operator.

“Throw” Operator

The throw operator is used for generating an error. The syntax of the throw operator is the following:

throw <error object >

Technically, anything can be used as an error object. It can even be a primitive such as a number or a string. However, it would be best if you used objects, preferably including name and message properties.

Also, JavaScript has multiple built-in constructors for standard errors: SyntaxErrorErrorReferenceError, and so on.

Their syntax looks like this:

let error = new Error(message);
// or
let error = new SyntaxError(message);
let error = new ReferenceError(message);

For-built-in errors the name property is the constructor name. The message should be taken from the argument, like this:

let error = new Error("Things happen");

console.log(error.name); // Error

console.log(error.message); // Things happen

JSON.parse generates this kind of error:

try {

  JSON.parse("{ bad json }");

} catch (e) {

  console.log(e.name); // SyntaxError

  console.log(e.message); // Unexpected token b in JSON at position 2

}

So, it’s a SyntaxError.

Now, let’s see how to throw the error:

let json = '{ "age": 25 }'; // incomplete data

try {

  let user = JSON.parse(json); // no errors

  if (!user.name) {

    throw new SyntaxError("Incomplete data: no name");

  }

  console.log(user.name);

} catch (e) {

  console.log("JSON Error: " + e.message); // JSON Error: Incomplete data: no name

}

Rethrowing

Above it was shown how to handle error using try..catch. But, it is possible that another unexpected error occurs inside the try {...} block. let’s check out a case:

let json = '{ "age": 35 }'; // not complete data
try {
  user = JSON.parse(json); // missed the "let" before the user
} catch (err) {
  console.log("JSON Error: " + err); // JSON Error: ReferenceError: no user is defined
}

So, programmers can make mistakes. Anytime a bug may be found out and lead to hacks.

Luckily, it is possible to find out what error you get, for example from the name, like here:

try {
  user = { /*...*/ };
} catch (e) {
  console.log(e.name); // "ReferenceError" for accessing an undefined variable
}

The catch is only capable of processing errors that it recognizes and rethrowing others.

See how catch handles only SyntaxError in the example below:

let json = '{ "age": 25 }'; // not complete data

try {

  let user = JSON.parse(json);

  if (!user.name) {

    throw new SyntaxError("Incomplete data: no name");

  }

  anyFunc(); // unexpected error

  console.log(user.name);

} catch (e) {

  if (e.name == "SyntaxError") {

    console.log("JSON Error: " + e.message);

  } else {

    throw e; // rethrow

  }

}

The technique of rethrowing can be explained in the following steps:

  1. All the errors are got by the catch.
  2. The error object err is analyzed in the catch(err) {...} block.
  3. In case you don’t know how to handle it, it will be thrown.

In the example below, you can see the process of rethrowing in a way that only SyntaxError is handled by the catch.

let json = '{ "age": 25 }'; // incomplete data

try {

  let user = JSON.parse(json);

  if (!user.name) {

    throw new SyntaxError("Incomplete data: no name");

  }

  anyFunc(); //error

  console.log(user.name);

} catch (e) {

  if (e.name == "SyntaxError") {

    console.log("JSON Error: " + e.message);

  } else {

    throw e; // rethrow

  }

}

The catch block is capable of catching only the errors that it knows how to work with. So, it skips all the others.

Let’s see another example where errors like that are taught by another level of stry..catch:

function readData() {

  let json = '{ "age": 25 }';

  try {

    //...

    anyFunc(); // error

  } catch (e) {

    //...

    if (e.name != 'SyntaxError') {

      throw e; // rethrow 

    }

  }

}

try {

  readData();

} catch (e) {

  console.log("External catch got: " + e);

}

The readData can handle only SyntaxError, while the outer try..catch is able handle anything.

Finally

The construct of try..catch has another code clause, known as finally.

In case finally is detected, it will execute cases like:

  1. If there were no errors, after try.
  2. If there were errors- after the catch.

The extended syntax of finally is the following;

try {
  ...  try to execute the code
}
catch (e) {
  ...handle errors
}

finally {
  ...execute always
}

Running this code will look as follows:

try {

  console.log('try');

  if (confirm('To make an error?')) BAD_CODE();

} catch (e) {

  console.log('catch');

} finally {

  console.log('finally');

}

The finally clause is generally used when one begins to do something and wishes to finalize it regardless of the outcome.

Let’s check out an example of using finally in case of successful execution of fib, and in case of an error:

let num = +prompt("Enter a positive integer number?", 20);

let diff, result;

function fib(n) {

  if (n < 0 || Math.trunc(n) != n) {

    throw new Error("Must not be negative as well as integer.");

  }

  return n <= 1 ? n : fib(n - 1) + fib(n - 2);

}

let start = Date.now();

try {

  result = fib(num);

} catch (e) {

  result = 0;

} finally {

  diff = Date.now() - start;

}

console.log(result || "error occurred");

console.log(`execution took ${diff}ms`);

So, the function can end with return or throw, it doesn’t matter. The finally clause can run in both of the cases.

Also, you should take into account that the variables such as result and diff in the code above should be before try..catch.

In case of putting let in the try block, it will be noticeable only inside of it.

Global Catch

The information in this part is not a part of core JavaScript but it can also be useful for you.

For fatal errors occurring outside try..catch, there is another way to deal with them. In browsers, you can appoint a function to the specific window.onerror property, which will execute in case there is an uncaught error. Its syntax will look like this:

window.onerror = function (message, url, line, column, err) {
  // ...
};

Here is an example of its usage:

<!DOCTYPE html>
<html>
  <title>Title of the document</title>
  <head></head>
  <body>
    <script>
      window.onerror = function(message, url, line, column, err) {
        alert(`${message}\n At ${line}:${column} of ${url}`);
      }; 
      function readData() {
        badFn(); // Error, something went wrong!
      }
      readData();
    </script>
  </body>
</html>

So, the primary role of the global handler window.onerror is not recovering the execution of the script but sending an error message to the developers.

Summary

The construct of try..catch allows handling runtime errors. With it, you can run the code and catch errors that might occur.

Also, the errors can be generated with the throw operator. As a rule, it is an error object that inherits from the built-in Error class.

Another essential pattern of error handling is rethrowing. Usually, the catch block knows how to handle specific error types, rethrowing errors that it doesn’t recognize.

Even in the case of not having try..catch, most of the environments allow setting up a global error handler for catching the errors that fall out. But, in case of inheriting, it is possible to use obj instanceof Error for identifying error objects. Hence, it is always better to inherit from it.

Reactions

Post a Comment

0 Comments

close