Transferred from: HTTPS://GITHUB.COM/DWQS/BLOG/ISSUES/49
Sometimes we ignore the details of error handling and stack traces, but these details are useful for writing libraries related to testing or error handling. For example, this week, for Chai has a very good PR, the PR greatly improved the way we deal with the stack, when the user's assertion fails, we will give more information (to help users navigate).
Proper handling of stack information allows you to erase useless data and focus only on useful data. At the same time, having a better understanding of the Errors object and its associated properties can help you make fuller use of Errors.
(function of) how the call stack works
Before talking about errors, let's understand the principle of the call stack (function):
When a function is called, it is pressed into the top of the stack, which is removed from the top of the stack after it has been run.
The data structure of the stack is a last-in, first-out, and is known as LIFO (last on, first out).
For example:
function C () { console.log (' C ');} function B () { console.log (' B '); C ();} function A () { console.log (' a '); b ();} A ();
In the example above, when function a runs, it is added to the top of the stack. Then, when function B is called inside function A, function B is pressed into the top of the stack. When function c is called inside function B, it is also pressed into the top of the stack.
When function C runs, a, B, and C (in this order) are included in the stack.
When function c is finished, it is removed from the top of the stack, and then the control flow of the function call goes back to function B. After function B finishes running, it is removed from the top of the stack, and then the control flow of the function call goes back to function a. Finally, function A is removed from the top of the stack after it finishes running.
To better demonstrate the behavior of the stack in the demo, you can use Console.trace () to output the current stack data in the console. At the same time, you want to read the output stack data in order from top to bottom.
function C () { console.log (' C '); function B () { console.log (' B '); C ();} function A () { console.log (' a '); b ();} A ();
Running the above code in Node's REPL mode will give you the following output:
Trace at C (repl:3:9) at B (repl:3:1) at -A (repl:3:1) at Repl: // <--for now feel-ignore anything below this point, these is Node ' s internals At realruninthiscontextscript (vm.js:22:35) at siginthandlerswrap (vm.js:98:12 ) at ContextifyScript.Script.runInThisContext (vm.js:24:12) at Replserver.defaulteval ( Repl.js:313:29) at bound (domain.js:280:14) at Replserver.runbound [as Eval] ( Domain.js:293:12)
As you can see, when you output from function C, the stack contains functions a, B, and C.
If you output the current stack data in function B after the function C has finished running, you will see that function C has been removed from the top of the stack, where only functions A and B are included in the stack.
function C () { console.log (' C ');} function B () { console.log (' B '); C (); Console.trace ();} function A () { console.log (' a '); b ();}
As you can see, after the function C runs, it has been removed from the top of the stack.
Trace at B (repl:4:9) at A (repl:3:1) at repl:1:1 // <-- For-now feel-ignore anything below this point, these is Node ' s internals at REALRUNINTHISCONTEXTSCR IPT (vm.js:22:35) at siginthandlerswrap (vm.js:98:12) at ContextifyScript.Script.runInThisContext (vm.js:24:12) at replserver.defaulteval (repl.js: 313:29) at bound (domain.js:280:14) at Replserver.runbound [as Eval] (domain.js: 293:12) at replserver.online (repl.js:513:10)
Error objects and Errors handling
An Error object is usually thrown when a program runs out of errors. The Error object can be used as a prototype for user-defined Error object inheritance.
The Error.prototype object contains the following properties:
These are the standard properties of Error.prototype, and in addition, different operating environments have their own specific properties. In environments such as Node, Firefox, Chrome, Edge, IE + +, Opera, and Safari 6+, the Error object has a stack property that contains the wrong stack trace. The stack track of an error instance contains all the stack structures after the constructor.
If you want to learn more about the specific properties of the Error object, you can read this article on the MDN.
In order to throw an error, you must use the Throw keyword. In order to catch a thrown error, you must use Try...catch to include code that might run out of error. The catch parameter is an instance of the error that was run out.
Like Java, JavaScript also allows the finally keyword to be used after try/catch. After the error has been processed, some cleanup work can be done in the finally statement block.
In syntax, you can use a try statement block, and then you do not have to follow the catch statement block, but you must follow the finally statement block. This means there are three different try statement forms:
Try...catch
Try...finally
Try...catch...finally
You can also embed a try statement inside a try statement:
Try {
Try {
Throw New // The error thrown here would be caught by its own ' catch ' clause
Catch (nestederr) { console.log (// This runs catch (Err) { Console.log (' This is not run. ') );}
You can also embed a try statement in a catch or finally:
Try { Throw NewError (' first error ');} Catch(Err) {Console.log (' First catch running '); Try { Throw NewError (' Second error '); } Catch(Nestederr) {Console.log (' Second catch running. '); }}Try{Console.log (' The try block is running ... ');} finally { Try { Throw NewError (' ERROR inside finally. ')); } Catch(Err) {Console.log (' Caught an error inside the finally block. '); }}
It is important to note that when you throw an error, you can simply throw a simple value instead of an Error object. Although this looks cool and is permissible, this is not a recommended practice, especially for developers who need to work with libraries and frameworks that have to deal with other people's code, because there is no standard to reference and no way to know what to get from the user. You can't trust users to throw Error objects, because they might not do it, but simply throw a string or numeric value. This also means that it is difficult to deal with stack information and other meta-information.
For example:
function runwithoutthrowing (func) { try { func (); Catch (e) { console.log (' There was a error, but I'll not throw it. ') ); Console.log (' The error\ ' s message was: ' + E.message }}}function Functhatthrowserror () { thrownew TypeError (' I am a TypeError. ') );}
Runwithoutthrowing (Functhatthrowserror);
If the user passes a parameter to the function runwithoutthrowing throws an Error object, the above code can catch the error normally. Then, if you throw a string, you will encounter some problems:
function runwithoutthrowing (func) { try { func (); Catch (e) { console.log (' There was a error, but I'll not throw it. ') ); Console.log (' The error\ ' s message was: ' + E.message }}}function Functhatthrowsstring () { throw ' I am a String. ' ;} Runwithoutthrowing (functhatthrowsstring);
Now the second console.log will output undefined. This may not seem important, but if you need to make sure that the Error object has a specific property or another way to handle the specific properties of the Error object (such as the practice of chai throws assertions), you have to do a lot of work to ensure that the program is running correctly.
Also, if the Error object is not thrown, the stack property is not obtained.
Errors can also be used as other objects, and you don't have to throw them, which is why most callback functions take Errors as the first argument. For example:
Const FS = require (' FS '); Fs.readdir ('/example/i-do-not-exist ',functioncallback (err, dirs) {if(ErrinstanceofError) { //' Readdir ' would throw a error because that directory does not exist //We'll now being able to use the Error object passed by it in our callback functionConsole.log (' Error Message: ' +err.message); Console.log (' See? We can use Errors without using try statements. '); } Else{console.log (dirs); }});
Finally, the Error object can also be used for rejected promise, which makes it easy to handle rejected promise:
New Promise (function(resolve, reject) { reject (new Error (' The Promise was rejected. ') ));}). Then (function() { console.log (' I am an error. ') );}). Catch (function(err) { ifinstanceof Error) { Console.log ( ' The promise is rejected with an error. ' ); Console.log (' Error Message: ' + err.message); }});
Processing stacks
This section is for the operating environment that supports error.capturestacktrace, such as Nodejs.
The first argument to Error.capturestacktrace is object, and the second optional parameter is a function. Error.capturestacktrace captures the stack information and creates a stack property in the first parameter to store the captured stack information. If the second argument is provided, the function acts as the end point of the stack call. Therefore, the stack information that is captured will display only the information that precedes the function call.
Use the following two demo to explain. First, only the captured stack information is stored in a common object:
Const MYOBJ = {};functionC () {}
functionB () {//Here we'll store the current stack trace into MYOBJError.capturestacktrace (MYOBJ); C ();}
functionA () {B ();}//First we'll call these functionsA ();
//Now let's see what's the stack trace stored into Myobj.stackConsole.log (myobj.stack);//This would print the following stack to the console://at B (repl:3:7) <--Since It's called Inside B, the B call is the last entry in the stack//At A (repl:2:1)//At repl:1:1 <--Node Internals below the line//At Realruninthiscontextscript (vm.js:22:35)//At siginthandlerswrap (vm.js:98:12)//At ContextifyScript.Script.runInThisContext (vm.js:24:12)//At Replserver.defaulteval (repl.js:313:29)//At bound (domain.js:280:14)//At Replserver.runbound [as Eval] (domain.js:293:12)//At replserver.online (repl.js:513:10)
As can be seen from the above example, call function A (which is pressed into the stack), then call function B inside a (which is pressed onto the stack and above a), and then capture the current stack information in B and store it in MYOBJ. Therefore, only the calls to A and B are included in the stack information of the console output. Information.
Now, let's pass a function as the second argument to Error.capturestacktrace, and look at the output information:
Const MYOBJ = {};functiond () {//Here we'll store the current stack trace into MYOBJ //This time we'll hide all the frames after ' B ' and ' B ' itselfError.capturestacktrace (MYOBJ, b);}functionC () {D ();}functionB () {C ();}functionA () {B ();}//First we'll call these functionsa ();//Now let's see what's the stack trace stored into Myobj.stackConsole.log (myobj.stack);//This would print the following stack to the console://At A (repl:2:1) <--as can see here we only get frames before ' B ' was called//At repl:1:1 <--Node Internals below the line//At Realruninthiscontextscript (vm.js:22:35)//At siginthandlerswrap (vm.js:98:12)//At ContextifyScript.Script.runInThisContext (vm.js:24:12)//At Replserver.defaulteval (repl.js:313:29)//At bound (domain.js:280:14)//At Replserver.runbound [as Eval] (domain.js:293:12)//At replserver.online (repl.js:513:10)//At Emitone (events.js:101:20)
When you pass function B as the second argument to Error.capturestacktracefunction, the output stack contains only the information before function B is called (although error.capturestacktracefunction is called in function D ), which is why only a is output on the console. The benefit of this approach is to hide some of the internal implementation details that are not relevant to the user.
JavaScript error handling and stack tracing