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; }
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:
- The name field must contain the value in the "jumping" register:
MyError
. - The code field must contain a value separated by an underscore, the characters must be in upper case:
SOMETHING_WRONG
. - Do not use the word
ERROR
in the code field. - The value in name is created for classifying errors, so it is better to use
ConnectionError
or MongoError
, instead of MongoConnectionError
. - The code value must be unique.
- The message field must be formed based on the value of the code and the passed variable parameters.
- For successful error handling, it is advisable to add additional information to the object itself.
- 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) {
Let's take a closer look at the CallSite object contained in the stack array. It has the following methods:
Method | Description |
---|
getThis | returns the value of this. |
getTypeName | returns the type of this as a string, usually the name field of the constructor. |
getFunction | returns a function. |
getFunctionName | returns the name of the function, usually the value of the name field. |
getMethodName | returns the field name of this object. |
getFileName | returns the name of the file (or browser script). |
getLineNumber | returns the line number. |
getColumnNumber | returns the offset in the string. |
getEvalOrigin | returns the place of the eval call if the function was declared inside the eval call. |
isTopLevel | whether the call is a call from the global scope. |
isEval | whether a call is a call from eval. |
isNative | whether the called method is internal. |
isConstructor | whether 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 {
And then in the error itself add a dynamic property to get the stack.
Object.defineProperty(MyError.prototype, 'stackAsArray', { get : function() {
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 ...