Custom Errors, Extending Error

Custom Errors, Extending Error

Custom Errors, Extending Error


In the course of developing something, it is necessary to have your own error classes for reflecting particular things that can go wrong. For errors in network operations, HttpError is necessary, for searching operations-NotFoundError, for database operations- DbError, and more. They should support basic error properties such as namemessage, and stack. Also, they can include other properties. For example, HttpError objects can contain a status code such as 403404, or 500.

JavaScript allows using throw with any argument. So, technically, custom errors don’t need inheritance from Error.

Extending Error

Now, let’s discover what extending error is. To be more precise, we can start from a function readUser(json), which reads JSON with user data.

So, a valid json looks like this:

let json = `{ "name": "John", "age": 25 }`;

Internally, JSON.parse is used.

In case it gets malformed json then it throws SyntaxError. Even if json is syntactically correct it doesn’t consider that the user is valid. It can miss essential data such as nameage, or other properties.

The readUser(json) function both reads JSON and checks the data. In case the format is wrong, then an error occurs. As the data is syntactically correct, it’s not a SyntaxError. It is a ValidationError.

For ValidationError it is necessary to inherit from the built-in Error class.

Here is the code for extending:

// "Pseudocode" for the built-in Error class 
class Error {
  constructor(message) {
    this.message = message;
    this.errorName = "Error"; // different names for different built-in error classes
    this.stack = < call stack > ; // non-standard, but most environments support it
  }
}

In the example below, ValidationError is inherited from it:

class ValidationError extends Error {

  constructor(message) {

    super(message);

    this.name = "ValidationError";

  }

}

function testFunc() {

  throw new ValidationError("Oops");

}

try {

  testFunc();

} catch (err) {

  console.log(err.message); // Oops

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

  console.log(err.stack); // a list of nested calls, with line numbers for each

}

Now, let’s try using it in createUser(json), like this:

class ValidationError extends Error {

  constructor(message) {

    super(message);

    this.name = "ValidationError";

  }

}

function createUser(json) {

  let user = JSON.parse(json);

  if (!user.age) {

    throw new ValidationError("No field: age");

  }

  if (!user.name) {

    throw new ValidationError("No field: name");

  }

  return user;

}

try {

  let user = createUser('{ "age": 20 }');

} catch (err) {

  if (err instanceof ValidationError) {

    console.log("Invalid data: " + err.message); // Invalid data: No field: name

  } else if (err instanceof SyntaxError) {//(*)

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

  } else {

    throw err; // unknown error, rethrow it

  }

}

So, in the code above the try..catch block handles ValidationError and SyntaxError from JSON.parse.

It is especially interesting how instanceof is used for checking a particular error in the (*) line.

The err.name can also look like here:

// ...
// instead of, err instanceof SyntaxError
} else if (err.name == "SyntaxError") { // (*)
}

However, the version with instanceof is better, as the next step is to extend ValidationError, making subtypes of it. The instanceof check will keep working for new inheriting classes.

Further Inheritance

The ValidationError class is very universal. Hence, various things can go wrong. For example, a property can be in a wrong format or it can be absent. Let’s consider the PropertyRequiredError for absent properties. It carries extra information about the missing property. Here is how it looks like:

class ValidationError extends Error {

  constructor(message) {

    super(message);

    this.name = "ValidationError";

  }

}

class PropertyRequiredError extends ValidationError {

  constructor(property) {

    super("Not a property: " + property);

    this.name = "PropertyRequiredError";

    this.property = property;

  }

}

function createUser(json) {

  let user = JSON.parse(json);

  if (!user.age) {

    throw new PropertyRequiredError("age");

  }

  if (!user.name) {

    throw new PropertyRequiredError("name");

  }

  return user;

}

// Example with try..catch

try {

  let user = createUser('{ "age": 20 }');

} catch (err) {

  if (err instanceof ValidationError) {

    console.log("Invalid data: " + err.message); // Invalid data: No property: name

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

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

  } else if (err instanceof SyntaxError) {

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

  } else {

    throw err; // unknown error, rethrow it

  }

}

The new class PropertyRequiredError is easy in usage. All you need is to do is passing the property name: new PropertyRequiredError(property). The constructor creates a human-readable message.

Please, consider that this.name in PropertyRequiredError constructor is assigned manually. So, assigning this.name = <class name> in each custom error class. For avoiding that the basic error class, which assigns this.name = this.constructor.name is made. It can inherit all the custom errors. It can be called ErrorName.

Here it is and other custom error classes:

class ErrorName extends Error {

  constructor(message) {

    super(message);

    this.name = this.constructor.name;

  }

}

class ValidationError extends ErrorName {}

class PropertyRequiredError extends ValidationError {

  constructor(property) {

    super("Not a property: " + property);

    this.property = property;

  }

}

// correct name

console.log(new PropertyRequiredError("field").name); // PropertyRequiredError

So, now custom errors are shorter.

Wrapping Exceptions

The purpose of the createUser function is reading the user data. In the process, various errors may occur.

The code calling createUser must handle the errors. In the example below, it applies multiple ifs in the catch block, checking the class and handling known errors and rethrowing the unknown ones.

The scheme will look as follows:

In the code above, there are two kinds of errors. Of course, they can be more. The technique, represented in this part is known as “wrapping extensions”. Let’s start at making a new class ReadError to present a generic data reading error. The readUser function catches the data reading errors, occurring inside it (for example, ValidationError and SyntaxError) and create ReadError uthuinstead.

The ReadError object keeps the reference to the original error inside the cause property.

try {
  //...
  createUser() // the potential error source
  //...
}
catch (err) {
  if (err instanceof ValidationError) {
    // handle validation error
  } else if (err instanceof SyntaxError) {
    // handle syntax error
  } else {
    throw err; // unknown error, rethrow it
  }
}

Let’s take a look at the code, which specifies ReadError demonstrating its usage in readUser and try..catch:

class ReadError extends Error {

  constructor(message, cause) {

    super(message);

    this.cause = cause;

    this.name = 'ReadError';

  }

}

class ValidationError extends Error { /*...*/ }

class PropertyRequiredError extends ValidationError { /* ... */ }

 

function validateUser(user) {

  if (!user.age) {

    throw new PropertyRequiredError("age");

  }

  if (!user.name) {

    throw new PropertyRequiredError("name");

  }

}

function createUser(json) {

  let user;

  try {

    user = JSON.parse(json);

  } catch (err) {

    if (err instanceof SyntaxError) {

      throw new ReadError("Syntax Error", err);

    } else {

      throw err;

    }

  }

  try {

    validateUser(user);

  } catch (err) {

    if (err instanceof ValidationError) {

      throw new ReadError("Validation Error", err);

    } else {

      throw err;

    }

  }

}

try {

  createUser('{bad json}');

} catch (e) {

  if (e instanceof ReadError) {

    console.log(e);

    // Error: SyntaxError:

    // Unexpected token b in JSON at position 1

    console.log("Error: " + e.cause);

  } else {

    throw e;

  }

}

The approach, described above is named “wrapping exceptions” as “low-level” exceptions are taken and wrapped into ReadError, which is more abstract.

Summary

In brief, we can state that, normally, it is possible to inherit from Error and other built-in classes. All you need is to take care of the name property, not forgetting to call super.

The instanceof class can also be applied to check for specific errors. It also operates with inheritance. At times when an error object comes from a third-party library, the name property can be used.

A widespread and useful technique is wrapping exceptions. With it, a function can handle low-level exceptions, creating high-level errors, instead of different low-level ones.

Reactions

Post a Comment

0 Comments

close