Please support this book: buy it or donate

22. Exception handling



This chapter covers how JavaScript handles exceptions.

As an aside: JavaScript didn’t support exceptions until ES3. That explains why they are used sparingly by the language and its standard library.

22.1. Motivation: throwing and catching exceptions

Consider the following code. It reads profiles stored in files into an Array with instances of class Profile:

  1. function readProfiles(filePaths) {
  2. const profiles = [];
  3. for (const filePath of filePaths) {
  4. try {
  5. const profile = readOneProfile(filePath);
  6. profiles.push(profile);
  7. } catch (err) { // (A)
  8. console.log('Error in: '+filePath, err);
  9. }
  10. }
  11. }
  12. function readOneProfile(filePath) {
  13. const profile = new Profile();
  14. const file = openFile(filePath);
  15. // ··· (Read the data in `file` into `profile`)
  16. return profile;
  17. }
  18. function openFile(filePath) {
  19. if (!fs.existsSync(filePath)) {
  20. throw new Error('Could not find file '+filePath); // (B)
  21. }
  22. // ··· (Open the file whose path is `filePath`)
  23. }

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:

  • In line B, we use a throw statement to indicate that there was a problem.
  • In line A, we use a try-catch statement to handle the problem.
    When we throw, the following constructs are active:
  1. readProfiles(···)
  2. for (const filePath of filePaths)
  3. try
  4. readOneProfile(···)
  5. openFile(···)
  6. if (!fs.existsSync(filePath))
  7. throw

throw walks up this chain of constructs, until it finds a try statement. Execution continues in the catch clause of that try statement.

22.2. throw

  1. throw «value»;

Any value can be thrown, but it’s best to throw instances of Error:

  1. throw new Error('Problem!');

22.2.1. Options for creating error objects

  • Use class Error. That is less limiting in JavaScript than in a more static language, because you can add your own properties to instances:
  1. const err = new Error('Could not find the file');
  2. err.filePath = filePath;
  3. throw err;
  • Use one of JavaScript’s subclasses of Error (which are listed later).

  • Subclass Error yourself.

  1. class MyError extends Error {
  2. }
  3. function func() {
  4. throw new MyError;
  5. }
  6. assert.throws(
  7. () => func(),
  8. MyError);

22.3. try-catch-finally

The maximal version of the try statement looks as follows:

  1. try {
  2. // try_statements
  3. } catch (error) {
  4. // catch_statements
  5. } finally {
  6. // finally_statements
  7. }

The try clause is mandatory, but you can omit either catch or finally (but not both). Since ECMAScript 2019, you can also omit (error), if you are not interested in the value that was thrown.

22.3.1. The catch clause

If an exception is thrown in the try block (and not caught earlier) then it is assigned to the parameter of the catch clause and the code in that clause is executed. Unless it is directed elsewhere (via return or similar), execution continues after the catch clause: with the finally clause – if it exists – or after the try statement.

The following code demonstrates that the value that is thrown in line A is indeed caught in line B.

  1. const errorObject = new Error();
  2. function func() {
  3. throw errorObject; // (A)
  4. }
  5. try {
  6. func();
  7. } catch (err) { // (B)
  8. assert.equal(err, errorObject);
  9. }

22.3.2. The finally clause

Let’s look at a common use case for finally: You have created a resource and want to always destroy it when your are done with it – no matter what happens while working with it. You’d implement that as follows:

  1. const resource = createResource();
  2. try {
  3. // Work with `resource`: errors may be thrown.
  4. } finally {
  5. resource.destroy();
  6. }

The finally is always executed – even if an error is thrown (line A):

  1. let finallyWasExecuted = false;
  2. assert.throws(
  3. () => {
  4. try {
  5. throw new Error(); // (A)
  6. } finally {
  7. finallyWasExecuted = true;
  8. }
  9. },
  10. Error
  11. );
  12. assert.equal(finallyWasExecuted, true);

The finally is always executed – even if there is a return statement (line A):

  1. let finallyWasExecuted = false;
  2. function func() {
  3. try {
  4. return; // (A)
  5. } finally {
  6. finallyWasExecuted = true;
  7. }
  8. }
  9. func();
  10. assert.equal(finallyWasExecuted, true);

22.4. Error classes and their properties

Quoting the ECMAScript specification:

  • Error [root class]
    • RangeError: Indicates a value that is not in the set or range of allowable values.
    • ReferenceError: Indicate 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.

22.4.1. Properties of error classes

Consider err, an instance of Error:

  1. const err = new Error('Hello!');
  2. assert.equal(String(err), 'Error: Hello!');

Two properties of err are especially useful:

  • .message: contains just the error message.
  1. assert.equal(err.message, 'Hello!');
  • .stack: contains a stack trace. It is supported by all mainstream browsers.
  1. assert.equal(
  2. err.stack,
  3. `
  4. Error: Hello!
  5. at Context.<anonymous> (ch_exception-handling.js:1:13)
  6. `.trim());