Using Error, try/catch, throw to make code easier to debug. They can also make the code structure clearer.

Error Overview

Here are some conclusions:

  • Error is Standard built-in objects.
  • Error objects are thrown when runtime errors occur.
  • Usually you create an Error object with the intention of raising it using the throw keyword. You can handle the error using the try...catch construct.
  • Error types: Besides the generic Error constructor, there are still: EvalError, TypeError, etc. in JavaScript.
  • The Error object can also be used as a base object for user-defined exceptions.

A cleaner and more consistent error handling way is that define your own error types deriving from Error to be able to throw new MyError() and use instanceof MyError to check the kind of error in the exception handler. This results in cleaner and more consistent error handling code.

Error Instance

Error.prototype.constructor

syntax: new Error(message, options) JavaScript only tries to read options.cause if options is an object.

Note: Error() can be called with or without new. Both create a new Error instance.

const x = Error("I was created using a function call!");
// above has the same functionality as following
const y = new Error('I was constructed via the "new" keyword!');

Error.prototype.name

The name data property of Error.prototype is shared by all Error instances. It represents the name for the type of error.

Error.cause

The cause data property of an Error instance indicates the specific original cause of the error.

It is used when catching and re-throwing an error with a more-specific or useful error message in order to still have access to the original error.

The value of cause can be of any type.

Error: message

The message data property of an Error instance is a human-readable description of the error.

The message property combined with the name property is used by the Error.prototype.toString() method to create a string representation of the Error.

Error.prototype.stack

This feature is non-standard, but it is de facto implemented by all major JavaScript engines, and the JavaScript standards committee is looking to standardize it.

  • The value of stack is string.
  • you can assume it exists and use it for debugging purposes.
  • The stack property of an Error instance offers a trace of which functions were called, in what order, from which line and file, and with what arguments.
  • The stack string proceeds from the most recent calls to earlier ones, leading back to the original global scope call.
  • Each JavaScript engine uses its own format for stack traces, but they are fairly consistent in their high-level structure.

Example:

function trace() {
  throw new Error("trace() failed");
}
function b() {
  trace();
}
function a() {
  b(3, 4, "\n\n", undefined, {});
}
try {
  a("first call, firstarg");
} catch (e) {
  document.getElementById("output").textContent = e.stack;
}

then you can see in output element:

Error: trace() failed
    at trace (<anonymous>:2:9)
    at b (<anonymous>:5:3)
    at a (<anonymous>:8:3)
    at <anonymous>:11:3
    at init (https://live.mdnplay.dev/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/stack/runner.html?id=using_the_stack_property:133:23)
    at https://live.mdnplay.dev/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/stack/runner.html?id=using_the_stack_property:146:17

How to Differentiate between similar errors

Sometimes a block of code can fail for reasons that require different handling, but which throw very similar errors (i.e. with the same type and message).

If you don’t have control over the original errors that are thrown, one option is to catch them and throw new Error objects that have more specific messages.

function doWork() {
  try {
    doFailSomeWay();
  } catch (err) {
    throw new Error("Failed in some way", { cause: err });
  }
  try {
    doFailAnotherWay();
  } catch (err) {
    throw new Error("Failed in another way", { cause: err });
  }
}

try {
  doWork();
} catch (err) {
  switch (err.message) {
    case "Failed in some way":
      handleFailSomeWay(err.cause);
      break;
    case "Failed in another way":
      handleFailAnotherWay(err.cause);
      break;
  }
}

A good way to extend Error

Read more at stackoverflow.

Best way is using class and extend. But you can still use function.

Use class

Use class and extend keywords to subclass Error constructor:

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

There is no need for this.stack = (new Error()).stack; trick thanks to super() call.

For ease of maintaining, use this.name = this.constructor.name; instead. Try this:

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

Use function

function MyError (message){
  this.message = message;
  this.name = 'MyError';
  Error.captureStackTrace(this);
}
// does the same magic as extends keyword
Object.setPrototypeOf(MyError.prototype, Error.prototype);

Test result

try{
  throw new MyError('this is test');
}catch(e){
  // true
  if(e instanceof Error){
    console.log(e)
  }
  // true
  if(e instanceof MyError){
    console.log(e)
  }
}

throw

The throw statement throws a user-defined exception. Execution of the current function will stop (the statements after throw won’t be executed), and control will be passed to the first catch block in the call stack. If no catch block exists among caller functions, the program will terminate.

Usually, throw statement throw an Error instance:

function getRectArea(width, height) {
  if (isNaN(width) || isNaN(height)) {
    throw new Error('Parameter is not a number!');
  }
}

try {
  getRectArea(3, 'A');
} catch (e) {
  console.error(e);
  // Expected output: Error: Parameter is not a number!
}

But it can throw any object. and catch statement can catch that object.

try…catch

The try...catch statement is comprised of a try block and either a catch block, a finally block, or both. The code in the try block is executed first, and if it throws an exception, the code in the catch block will be executed. The code in the finally block will always be executed before control flow exits the entire construct.

A common use case for this is to only catch (and silence) a small subset of expected errors, and then re-throw the error in other cases:

try {
  myRoutine();
} catch (e) {
  if (e instanceof RangeError) {
    // statements to handle this very common expected error
  } else {
    throw e; // re-throw the error unchanged
  }
}

Execute order in Nested try blocks

try {
  try {
    throw new Error("oops");
  } finally {
    console.log("finally");
  }
} catch (ex) {
  console.error("outer", ex.message);
}
// Logs:
// "finally"
// "outer" "oops"

try {
  try {
    throw new Error("oops");
  } catch (ex) {
    console.error("inner", ex.message);
  } finally {
    console.log("finally");
  }
} catch (ex) {
  console.error("outer", ex.message);
}
// Logs:
// "inner" "oops"
// "finally"

try {
  try {
    throw new Error("oops");
  } catch (ex) {
    console.error("inner", ex.message);
    throw ex;
  } finally {
    console.log("finally");
  }
} catch (ex) {
  console.error("outer", ex.message);
}
// Logs:
// "inner" "oops"
// "finally"
// "outer" "oops"

FAQ

  • what is stack proerpty.