Subtleties of nodejs. Part II: Dealing with Errors

  • Tutorial
Error handling in JS is also a headache. I will not be mistaken if I say that mistakes are the weakest point of the whole language. Moreover, the problem consists of two others: the difficulty of catching errors in asynchronous code and the poorly designed Error object. And if many articles are devoted to the first problem, then many are undeservedly forgotten about the second. In this article I will try to make up for the shortcoming and consider the Error object more closely.

Root of evil


The implementation of the error object in JS is one of the worst that I have ever encountered. Moreover, the implementation itself is different in various engines. The object was designed (and developed) as if neither before nor after the occurrence of JS with errors they did not work at all. I don’t even know where to start. This object is not programmable, as all important values ​​are glued strings. There is no call stack capture mechanism and error extension algorithm.

The result of this is that each developer is forced to make decisions on a case-by-case basis, but, as scientists have proved, people are uncomfortable with the choice, so very often errors are simply ignored and fall into the main stream. Also, quite often, instead of an error, you can get an Array or Object, designed "at your discretion." Therefore, instead of a single error handling system , we are faced with a set of unique rules for each individual case.
And these are not only my words, the same TJ Holowaychuck wrote about this in his letter saying goodbye to the nodejs community.

How to solve the problem? Create a unified strategy for generating and processing error messages! Google developers offer V8 users their own set of tools that make this task easier. And so let's get started.

Myerror


Let's start by creating our own error object. In classical theory, all you can do is create an instance of Error and then complement it, this is how it looks:

var error = new Error('Some error');
error.name = 'My Error';
error.customProperty = 'some value';
throw error;

And so for each case? Yes! Of course, one could create the MyError constructor and set the necessary field values ​​in it:
function MyError(message, customProperty) {
    Error.call(this);
    this.message = message;
    this.customProperty = customProperty;
}

But this way we get an extra error record on the stack, which will complicate the search for errors for other developers. The solution is the Error.captureStackTrace method. It receives two values ​​at the input: the object into which the stack will be written and the constructor function, the record of which must be removed from the stack.
function MyError(message, customProperty) {
    Error.captureStackTrace(this, this.constructor);
    this.message = message;
    this.customProperty = customProperty;
}
// Для успешного сравнения с помощью ...instanceof Error:
var inherits = require('util').inherits;
inherits(MyError, Error);


Now, wherever an error pops up in the stack, the address of the new Error call will be in the first place.

message, name and code


The next point in solving the problem is identifying the error. In order to programmatically process it and decide on further actions: give the user a message or shut down. The message field does not provide such opportunities: parsing a message with a regular expression does not seem reasonable. How, then, to distinguish an invalid parameter error from a connection error? In nodejs itself, the code field is used for this. At the same time, the standard requires the use of the name field to classify errors . But they are used in different ways, so I recommend using the following rules for this:

  1. Field name should contain the value "galloping" Register: MyError.
  2. Code field shall contain the value divided by underlined characters must be uppercase: SOMETHING_WRONG.
  3. Do not use the word in the code field ERROR.
  4. The value in name was created to classify errors, so it is better to use ConnectionErroreither MongoError, instead MongoConnectionError.
  5. The code value must be unique.
  6. The message field should be formed on the basis of the code value and the passed variable parameters.
  7. For successful error handling, it is advisable to add additional information to the object itself.
  8. Additional values ​​should be primitive: you should not pass a database connection to the error object.


Example:

To create a report about a file reading error due to the file being missing, you can specify the following values: FileSystemErrorfor nameand FILE_NOT_FOUNDfor code, as well as an error, add a field file.

Stack processing


Also in V8 there is a function Error.prepareStackTracefor getting a raw stack - an array of CallSite objects. CallSite - these are objects that contain information about the call: the error address (method, file, line) and links directly to the objects themselves whose methods were called. Thus, in our hands is a fairly powerful and flexible tool for debugging applications.
In order to get the stack, you need to create a function that receives two arguments as an input: the error itself and an array of CallSite objects, you need to return the finished string. This function will be called for each error when accessing the stack field. The created function must be added to Error itself as prepareStackTrace:
Error.prepareStackTrace = function(error, stack) {
    // ...
    return error + ':\n' + stackAsString;
};

Let's take a closer look at the CallSite object contained in the stack array. It has the following methods:
MethodDescription
getThisreturns the value of this.
getTypeNamereturns the type this as a string, usually the name field of the constructor.
getFunctionreturns a function.
getFunctionNamereturns the name of the function, usually the value of the name field.
getMethodNamereturns the field name of the this object.
getFileNamereturns the name of the file (or browser script).
getLineNumberreturns the line number.
getColumnNumberreturns the offset in the string.
getEvalOriginreturns the place of the call to eval if the function was declared inside the call to eval.
isTopLevelWhether a call is a call from a global scope
isEvalIs the call a call from eval.
isNativewhether the called method is internal.
isConstructorwhether the method is a constructor call.

As I said above, this method will be called once for each error. In this case, the call will occur only when accessing the stack field. How to use it? You can add a stack as an array to the error inside the method:
Error.prepareStackTrace = function(error, stack) {
    error._stackAsArray = stack.map(function(call){
        return {
            // ...
            file : call.getFileName()
        };
    });
    // ...
    return error + ':\n' + stackAsString;
};

And then add the dynamic property to get the stack to the error itself.
Object.defineProperty(MyError.prototype, 'stackAsArray', {
    get : function() {
        // Инициируем вызов prepareStackTrace
        this.stack;
        return this._stackAsArray;
    }
});

So we got a full report, which is available programmatically and allows you to separate system calls from module calls and from application calls for detailed analysis and processing. Immediately make a reservation that there can be a lot of subtleties and questions in the analysis of the stack, so if you want to figure it out, I advise you to dig yourself.
All changes to the API should be tracked on the v8 wiki page dedicated to ErrorTraceAPI.

Conclusion


I want to end here, probably for the introductory article this is enough. Well, I hope that this material will save someone time and nerves in the future. In the next article I will tell you how to make work with errors comfortable using the approaches and tools described in the article.

UPD . To everyone who is waiting for revelations about catching asynchronous errors: to be continued ...

Also popular now: