Chapter 1. Errors

Errors. Everyone has them. No one is perfect. Things go wrong. Sometimes the error could have been anticipated, other times, not so much. To modify a cliche, it’s not so much the error as what you do with it that matters.

What should we do with our errors, then? The default behavior is for JavaScript to die at the point of the error, exiting with a stack trace. We can capture an error, react to it, modify it, re-throw it, even hide it if we choose. Returning to a theme of this cookbook: just because we can, does not mean we should. Our guiding light is effective, efficient, reusable JavaScript programming. Let’s look at Errors under this light.

JavaScript is not robust at error handing in general. The language has the Error object, which is flexible enough. But catching errors with try-catch lacks features found in other languages. We cannot catch errors by their type. We cannot (easily) filter by error. Further, in the browser, it is rare that we can recover from an error. Rare enough that the recipes below deal with the few common error situations from which an application can recover. In Node.js, throwing errors causes enough problems that an entirely different pattern of error handling involed. We will look at Node’s peculiar form of error “handling” as well.

Our goal should be to make our programming experience better. Instead of our JavaScript engine dying horribly with an incomprehensible error, we can add and modify information about the error, illuminating the path for the person (likely ourselves) tasked with fixing the error. Allow the failure to happen, but clarify why the error occurred and who raised it.

1.1 Using Errors

JavaScript has eight Error types. The parent type is the aptly-named Error. There are seven subtypes, which we will look at in the next recipe. What does an Error give us? We can count on three properties: a constructor, name, and message. These are all available to subclasses of Error as well. We also have access to a toString() method which will usually return the message property. If we are using the V8 JavaScript engine (Chrome, Node.js, possibly others), we can use captureStackTrace() as well. More on this soon. Let’s start with a standard JavaScript Error object.

Problem

You want to create, throw, and catch a standard error

Solution

Create an instance of Error, or a subtype, throw it, and catch it later on. Perhaps that is too brief a description. Start by creating an instance of Error. The constructor takes a string as an argument. Pass something useful and indicative of what the problem was. Consider keeping a list of the various error strings used in the application so that they are consistent and informative. Use the throw keyword to throw the instance you created. Then catch it in a try-catch block.

function willThrowError() {
  if ( /* something goes wrong */) {
    // Create an error with useful information.
    // Don't be afraid to duplicate something from the stack trace, like the method name
    throw new Error(`Problem in ${method}, ${reason}`);
  }
}

// Somewhere else in your code

try {
  willThrowError();
  // Other code will not be reached
} catch (error) {
  console.error('There was an error: ', error);
}

Discussion

We can create an error either with the new keyword before the Error or not. If Error() is called without new preceding it, Error() acts as a function which returns an Error object. The end result is the same. The Error constructor takes one standard argument: the error message. This will be assigned to the error’s message property. Some engines allow for additional non-standard arguments to the constructor, but these are not part of a current ECMAScript spec and are not on track to be on a future one. Wrapping the code that will throw an error in a try-catch block allows us to catch the error within our code. Had we not done so, the error would have propagated to the top of the stack and exited JavaScript. Here, we are reporting the error to the console with console.error. This is more or less the default behavior, though we can add information as part of the call to console.error.

1.2 Capturing Errors by their subtypes

JavaScript has seven subtypes of Error. We can check for a subtype of error to determine the kind of error raised by a problem in our code. This may illuminate the possibility of recovery, or at least give us a little more information about what went wrong.

The seven Error subtypes:

  • EvalError: thrown by use of the built-in function eval()

  • InternalError: Internal to the JavaScript engine; not part of the ECMAScript spec, but used by engines for non-standard errors

  • RangeError: A value is outside of its valid range

  • ReferenceError: Raised when encountering a problem trying to dereference an invalid reference

  • SyntaxError: A problem with the syntax of evaluated code, including JSON

  • TypeError: A variable or parameter is of an unexpected or wrong type

  • URIError: Raised by problems with encodeURI() and decodeURI()

Problem

How do we catcn errors by their subtype?

Solution

In your catch block, check the specific error type

try {
  // Some code that will raise an error
} catch (err) {
  if (err instanceof RangeError) {
    // Do something about the value being out of range
  } else if (err instanceof TypeError) {
    // Do something about the value being the wrong type
  } else {
    // Rethrow the error
    throw err;
  }
}

Discussion

We may need to respond to specific subtypes of Error. Perhaps the subtype can tell us something about what went wrong with the code. Or maybe our code threw a specific Error subtype on purpose, to carry additional information about the problem in our code. Both TypeError and RangeError lend themselves to this behavior, for example. The problem is that JavaScript permits only one catch block, offering neither opportunity nor syntax to capture errors by their type. Given JavaScript’s origins, this is not surprising, but it is nonetheless inconventient.

Our best option is to check the type of the Error ourselves. Use an if statement with the instanceof operator to check the type of the caught error. Remember to write a complete if-else statement, otherwise your code may accidentally swallow the error and suppress it. In the code above, we assume that we cannot do anything useful with error types that are neither RangeErrors nor TypeErrors. We re-throw other errors, so that code higher up the stack than this can choose to capture the error. Or, as is appropriate, the error could bubble to the top of the stack, as it normally would.

We should note: handling errors, even by their subtype, is fraught with difficulties in JavaScript. With rare exceptions, an error raised by your JavaScript engine cannot be recovered from in a meaningful sense. It is better to try to bring useful information about the error to the attention of the user, rather than to try to bring JavaScript back into a correct state (which may, in fact, be impossible!).

For example: Consider an EvalError. What are we, as coders, going to do if there’s an EvalError or a SyntaxEror? It seems that we should go an fix our code. What about a ReferenceError? Code cannot recover from trying to dereference an invalid referent. We might be able to write a catch block that returns something more informative than “Attempted to call getFoo() on undefined”. But we cannot determine what the original code intended. We can only repackage the error and exit gracefully (if possible). In the next section, we will look at two error types that offer the possibility of recovery, as well as throwing our own, more useful error types.

1.3 Throwing useful errors

Given the limits of JavaScript and error handling, are there Error subtypes worth using? Several of the subtypes of Error are limited to very specific cases. But two, the RangeError, and the TypeError show some promise.

Problem

You want to throw useful Error subtypes

Solution

Use TypeError to express an incorrect parameter or variable type

function calculateValue(x) {
  if (typeof x !== 'number') {
    throw new TypeError(`Value [${x}] is not a number.`);
  }

  // Rest of the function
}

Use RangeError to express that a parameter or value is out of range

function setAge(age) {
  const upper = 125;
  const lower = 18;
  if (age > 125 || age < 18) {
    throw new RangeError(`Age [${age}] is out of the acceptable range of ${lower} to ${upper}.`);
  }
}

Discussion

If you are going to use Errors to express incorrect states for your application, the TypeError and RangeError hold some promise. TypeError covers issues where the expected value was of the wrong type. What constitutes a “wrong” type? That’s up to us as the designers. Our code might expect one of JavaScript’s “primitive” values returned by a call to typeof. If our function receives an unantipated value type, we could throw a TypeError. We can say the same with instanceof for object types. These are not hard limits on when we might throw a TypeError, but they are good guidelines.

Going further, if our function received the right kind of value but it was outside of an acceptable range, we can throw a RangeError. Note that RangeErrors are specifically bound to numeric values. Put another way, we should not throw a RangeError when expecting a string and receiving one that is too short or too long. In the case of either RangeErrors or TypeErrors, make sure your error message is informative. Include the given value and information about the expected value.

Do not use errors for to validate forms or other input. Errors often do not show up in the HTML of a page, as they are re-routed to the console. Even if they do show up in the visible part of the page, they often contain impenetrable and weird content which will confuse and annoy the common user. And if they are not anticipated as part of your rendered content, they can throw off the rendering of the rest of the view. Invalid user data should be an expected state of your code, and should be handled in a user-friendly manner. There are both native APIs for form validation, as well as customizable, event-based hooks for validation. Use those instead. Errors in JavaScript indicate a state where your code received input that is just wrong and should be corrected.

1.4 Throwing custom errors

The Error type might be too broad. Most subclasses are too specific, or too limiting. What if we want to create our own Error types? How should we subclass Error?

Problem

How do we create our own Error subtypes?

Solution

Create a subtype of Error by subclassing Error using the ECMAScript 2015 class inheritance syntax

class CustomError extends Error {
  constructor(customProp='customValue', ...params) {
    super(...params);

    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, CustomError);
    }

    this.name = 'CustomError';
    this.customProp = customProp;
  }
}

Discussion

When subclassing Error, we should keep in mind two possibly competing concerns: stayign within the bounds of a typical JavaScript error, and expressing enough information for our customized error. In the former case, do not attempt to recreate the errors or exceptions of your second favorite language. Do not over-extend JavaScript’s Error type with extra methods and properties. A later programmer using your new Error subtype should be able to make reasonable assumptions about the subtype’s API. They should be able to access standard information in the standard way.

Your subclass constructor can take custom properties, which should be distinct from the standard argument of an error message. Keep in mind that Firefox and some other browsers expect the first three arguments to the Error constructor to be a message string, a filename, and a line number. If your subclass’s constructor takes custom arguments, add the to the front of the parameters list. This will make capturing standard arguments easier, as we can use ECMAScript 2015’s rest parameter feature to vaccuum up any remaining arguments.

In the constructor for your custom error, call super() first, passing any standard parameters. Because your code might run on V8 (either Chrome or Node.js), properly set this Error’s stack trace. Check for the captureStackTrace method, and, if present, call Error.captureStackTrace passing it a reference to the current instance (as this) and your CustomError class.

Set the name property of your custom error subclass. This ensures that, when reporting to the console, your error will carry the name of your subclass, instead of the more generic Error class.

Set any custom properties as necessary.

1.5 Handling JSON parsing errors

JavaScript Object Notation has become a popular, portable data format thanks in part to the popularity of JavaScript. There are two native functions availabe for JSON processing: for JavaScript-to-JSON conversion, use JSON.stringify(jsObject). To deserialize a JSON string into a JavaScript object (or string, or array, etc.) use JSON.parse(jsonString). Conversion of JavaScript to a JSON string should not raise any errors, barring issues with the JavaScript engine implementing JSON.stringify. On the other hand, converting a JSON string into JavaScript has many potential issues. The JSON could be malformed, or could just not be JSON at all! JavaScript raises error with JSON parsing as SyntaxErrors, which is sub-optimal. A SyntaxError is raised when JavaScript tries to interpret syntactically invalid code. While technically accurate, this lacks detail in the case of JSON parsing.

Problem

How should we handle parsing of bad or malformed JSON data?

Solution

Create a custom subtype of SyntaxError which is raised on issues with JSON parsing.

class JSONParseError extends SyntaxError {
  constructor(...params) {
    super(...params);

    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, JSONParseError);
    }

    this.name = 'JSONParseError';
  }
}

function betterParse(jsonString) {
  try {
    JSON.parse(jsonString);
  } catch (err) {
    if (err instanceof SyntaxError) {
      throw new JSONParseError(err);
    } else {
      throw err;
    }
  }
}

Discussion

We would like to log a specific error type when JSON.parse encounters an error. We can create a class, JSONParseError, which is a subclass of SyntaxError, for problems with JSON parsing. The class itself does not do much, other than establishing the name and type JSONParseError. But consumers of this API can now test for JSONParseError as opposed to the too-general SyntaxError. Logging will improve as well.

Remembering to re-throw SyntaxErrors as JSONParseErrors anytime we parse JSON strings is tedious. Instead, we will call betterParse(jsonString) which does the work for us. The betterParse function wraps the call to JSON.parse() in a try block, safely catching any SyntaxErrors. If a SyntaxError is raised, we will re-package it as a JSONParseError. If some other error is raised (it could happen), we will pass that error along as-is, with no modification or re-packaging.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.183.14