Error handling

Error objects

Normally when an error occurs, the JavaScript engine creates a new error object, throws it and stops further execution of the program. The error can be viewed in the browser's console. Besides a generic Error type, there are other core error types in JavaScript, like TypeError, ReferenceError or SyntaxError. The error type thrown depends on the error that occurs.

A SyntaxError will normally be thrown during parsing/compiling of the code if the code contains invalid syntax. After parsing/compiling, with valid syntax, the program will be run by the computer's processor. Errors that occur during that phase are called runtime errors or exceptions. Runtime errors can be "caught" and handled by the code itself, as we will discover in this chapter. It is also possible to include statements in the code that throw programmer-defined exceptions.

throw

A statement with keyword throw, followed by an expression, throws an exception. The expression will be thrown. The execution of the rest of the program will terminate (unless there is a catch statement, which will be explained later this chapter).

The thrown exception (the statement's expression) may be any object or primitive, but generally it is better to use an instance of one of the exception types. The generic Error object, as well as all its possible exception types, have their own constructor, so an exception that is an Error object can be created with the keyword new.


function check(n) {
  if (!(n >= -500 && n <= 500)) {
    throw new RangeError("The argument must be between -500 and 500.");
  }
};

check(1000); // throws: RangeError: The argument must be between -500 and 500.

An error object has two main properties: name, which is the error type (RangeError in the example above) and message, which is a string describing the error. These properties can be used in the catch block, as we will discover later this chapter.

try...catch

As mentioned before, we can "catch" an exception and handle it in some desired way. We can accomplish this by using a try...catch statement. A try...catch statement consists of a try block, followed by either a catch block, a finally block or both. The try block is executed first. If execution of the try block throws an exception, execution of this block stops and execution is transfered to the catch block (if it exists) and the exception will be passed to the catch block. So, if the try does not throw an error, the catch block will be ignored.

The code in the finally block will always execute, regardless of whether the try block threw an exception or not. A finally block is to finalize or "cleanup" the process that started in the try block.


try {
  nonExistentFunction();
} catch (error) {
  console.error(error);
};

console.log("Execution continues...");

// Logs: 
// ReferenceError: nonExistentFunction is not defined
// "Execution continues..."

In the above example the thrown exception was caught. Execution of the try block stopped when the error occurred, but execution of the overall program did not stop! Without a catch block, the program would have stopped! Only a finally block would still have been executed. Without a catch block, the finally block is mandatory: only a try statement, without a catch or finally block, is not allowed.


try {
  nonExistentFunction();
} finally {
  console.log("Executed anyhow");
};

console.log("Execution continues...");

// logs:
// "Executed anyhow"
// Uncaught ReferenceError: nonExistentFunction is not defined

A try...finally statement like above may be useful when, for instance, the try...finally statement is nested within the try block of an other try...catch statement and all the possible errors should be caught by the outer catch block.


try {
  nonDeclaredVar = 3;
  try {
    nonExistentFunction();
  } finally {  };
} catch (err) {
  console.error(`Oops, a ${err.name} occurred. The error is: ${err.message} `);
};

// In strict mode it logs:
// "Oops, a ReferenceError occurred. The error is: assignment to undeclared variable nonDeclaredVar"

// In non-strict mode it logs:
// "Oops, a ReferenceError occurred. The error is: nonExistentFunction is not defined "

BTW: In non-strict mode nonDeclaredVar declares as an implicit global.

BTW: As previously mentioned: an error object has two main properties: name and message. They are both used here in the catch block.

Rethrowing

Typically a try...catch statement is used when the programmer assumes a reasonable possibility that a specific error will occur while executing a certain portion of code. In the catch block that specific error will be handled. But it is also possible that other, unexpected errors occur during the execution of the try block. They will also be "handled" by the catch block, but the code in this block was not intended for other errors than the specific expected error. In addition, the program will continue to run, even if a potentially malicious error has occurred.


function check(n) {
  if (!(n >= -500 && n <= 500)) {
    throw new RangeError("The argument must be between -500 and 500.");
  }
};

try {
  nonExistentFunction();
  check(1000);
} catch (error) {
  console.error(error);
  // handle a possible RangeError occurring in check()...
};

console.log("Execution continues...");

// logs:
// ReferenceError: nonExistentFunction is not defined
// "Execution continues..."

In the example above the execution of the program continues with the false assumption that function nonExistentFunction() executed. The ReferenceError was caught, but likely not handled the proper way. The catch block was designed to handle a RangeError in the check function, but this function did not even get to be called. This all went south because of an unexpected error: the calling of a non-existent function. This can be solved by filtering the error, handling the intended error and rethrowing any other error.


function check(n) {
  if (!(n >= -500 && n <= 500)) {
    throw new RangeError("The argument must be between -500 and 500.");
  }
};

try {
  nonExistentFunction();
  check(1000);
} catch (error) {
  if (error instanceof RangeError) {
    console.error(error);
    // handle a possible RangeError occurring in check()...
  } else {
    throw error
  }
};

console.log("Execution continues...");

// logs: Uncaught ReferenceError: nonExistentFunction is not defined

A more elaborate example:


function check(n) {
  if (n <= -500 || n >= 500) {
    throw new RangeError(`${n} is not between -500 and 500`);
  }
  return n;
};

let succeeded = false;
while (!succeeded) {
  try {
    let num = Math.floor(Math.random() * 2000 - 1000); // random number between -1000 and 1000
	console.log(check(num));
	succeeded = true;
  } catch (e) {
    if (e instanceof RangeError) {
      console.log(`${e.message}. Trying a new number...`);
    } else {
	  throw e;
    }
  }
}  
// One possible log:
// "-793 is not between -500 and 500. Trying a new number..."
// "600 is not between -500 and 500. Trying a new number..."
// -33

Asynchronous errors

Cases where programmers may expect possible errors to occur often involve loading of external resources, such as a file that contains data that the program needs. The loading itself may be unsuccessful or the data may contain invalid syntax. Errors like these cannot be caught and handled with a regular try...catch statement. The loading takes time, and in the meantime the execution of the program, including the try...catch statement, continues. By the time a possible error occurs, the program already finished the try...catch statement (and likely skipped the catch block). Functions like this, functions that take their time while the rest of the program execution continues, are called asynchronous functions. JavaScript has its own way to handle them, with constructs that are somewhat similar to the try...catch statement. Later in this tutorial asynchronous JavaScript will be explained in much more detail. For now a simple example of a setTimeout function, which is asynchronous, used in a try...catch statement.


try {
  setTimeout(function() {
    nonExistentFunction();
  }, 1000);
} catch (e) {
  console.error(`Whoops, a ${e.name} occurred. The error is: ${e.message} `);
};

// logs after one second:
// Uncaught ReferenceError: nonExistentFunction is not defined

The catch block was skipped. Note that placing the try...catch statement in the setTimeout function is a way to solve the problem.