📜 ⬆️ ⬇️

Subtleties nodejs. Part II: Work with errors

Error handling in JS is still a headache. I’m not 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 object Error. And if a lot of articles are devoted to the first problem, then many undeservedly forget about the second one. In this article I will try to fill the gap and look at the Error object more closely.

Root of evil


The implementation of the error object in JS is one of the worst I’ve ever met. Moreover, the implementation itself is different in different engines. The object is designed (and developed) as if neither before nor after the occurrence of JS with errors did not work at all. I don't even know where to start. This object is not programmatically interpreted, since 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 their own decisions on a case-by-case basis, but, as scientists have proven, the choice makes people uncomfortable, so very often errors are simply ignored and fall into the mainstream. Also, quite often, instead of an error, you can get Array or Object, designed "at its 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 just 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 the formation and processing of an error message! Google developers offer V8 users their own set of tools that facilitate this task. And so we begin.
')

Myerror


Let's start by creating your own error object. In the classical theory, all you can do is create an instance of Error, and then supplement it, this is what it looks like:

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

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

But this way we will get an extra error record on the stack, which will complicate the search for errors by other developers. The solution is the Error.captureStackTrace method. It receives two values ​​at the input: the object to which the stack will be written and the constructor function, the record of which should 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 the error in the stack comes up, the first place will be the address of the call to the new Error.

message, name and code


The next item in the solution is to identify the error. In order to process it programmatically and decide on further actions: give a message to the user or complete the work. The message field does not provide such opportunities: parsing a message with a regular expression does not seem reasonable. How, then, to distinguish the error of the wrong parameter from the connection error? In the nodejs itself, the code field is used for this. In this case, the standard for classifying errors is prescribed to use the name field . But they are used differently, so I recommend using the following rules for this:

  1. The name field must contain the value in the "jumping" register: MyError .
  2. The code field must contain a value separated by an underscore, the characters must be in upper case: SOMETHING_WRONG .
  3. Do not use the word ERROR in the code field.
  4. The value in name is created for classifying errors, so it is better to use ConnectionError or MongoError , instead of MongoConnectionError .
  5. The code value must be unique.
  6. The message field must be formed based on the value of the code and the passed variable parameters.
  7. For successful error handling, it is advisable to add additional information to the object itself.
  8. Additional values ​​must be primitives: it is not necessary to transfer a database connection to the error object.


Example:

To create a file read error report for the reason that the file is missing, you can specify the following values: FileSystemError for name and FILE_NOT_FOUND for code , as well as the file field should be added to the error.

Stack handling


Also in V8 there is an Error.prepareStackTrace function for getting a raw stack — an array of CallSite objects. CallSite are objects that contain information about the call: the error address (method, file, string) and links directly to the objects themselves whose methods were called. Thus, in our hands is quite a powerful and flexible tool for debugging applications.
In order to get the stack, you need to create a function that takes two arguments as input: the error itself and the array of CallSite objects, you must 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 of 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 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 eval call if the function was declared inside the eval call.
isTopLevelwhether the call is a call from the global scope.
isEvalwhether a call is 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? Inside the method, you can add to the error the stack as an array:
 Error.prepareStackTrace = function(error, stack) { error._stackAsArray = stack.map(function(call){ return { // ... file : call.getFileName() }; }); // ... return error + ':\n' + stackAsString; }; 

And then in the error itself add a dynamic property to get the stack.
 Object.defineProperty(MyError.prototype, 'stackAsArray', { get : function() { //   prepareStackTrace this.stack; return this._stackAsArray; } }); 

So we received a full-fledged report that is available programmatically and allows us to separate the system calls from the module calls and from the calls of the application itself for detailed analysis and processing. I’ll just make a reservation that there can be a lot of subtleties and questions when analyzing a stack, so if you want to figure it out, I advise you to dig in on your own.
All changes to the API should be monitored on the v8 dedicated to ErrorTraceAPI.

Conclusion


At this point I want to finish, 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 . Everyone who is waiting for revelations about catching asynchronous errors: to be continued ...

Source: https://habr.com/ru/post/244523/


All Articles