Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at the jQuery Conference 2009 (Cambridge, MA) with: Rey Bango
Ben Nadel at the jQuery Conference 2009 (Cambridge, MA) with: Rey Bango@reybango )

In Node.js, The error.stack Property Will Represent The Point In The Code At Which new Error() Was Called - Not When The Error Is Thrown

By Ben Nadel on

Yesterday, I was trying to help my co-worker figure out why one of his Node.js containers was throwing errors. When looking at the logs, however, I discovered that the errors weren't all that helpful. The stack-traces in the log only pointed to the line of code on which the Error objects were being instantiated - they were not pointing to the portion of the control flow in which the error was actually being thrown. Most of the time, these two gestures - construction and throwing - go hand-in-hand; however, in this case they were separated, which created a confusing and obfuscated audit trail.



To be clear, this is not a bug. Nor is it an unexpected behavior. If look at the Node.js documentation for the Error() constructor, it is stated clearly:

Creates a new Error object and sets the error.message property to the provided text message. If an object is passed as message, the text message is generated by calling message.toString(). The error.stack property will represent the point in the code at which new Error() was called. Stack traces are dependent on V8's stack trace API. Stack traces extend only to either (a) the beginning of synchronous code execution, or (b) the number of frames given by the property Error.stackTraceLimit, whichever is smaller.

To see why this is important to understand, consider the following Node.js (v8.3.0) code:

  • try {
  • // CAUTION: The .stack property of this error will indicate that the error occurred
  • // on line-6 - NOT on the subsequent line in which the error is throw()n.
  • var error = new Error( "Instantiated on Line-6." );
  • var rando = Math.random();
  • if ( rando > 0.6 ) {
  • error.randomValue = "high";
  • throw( error );
  • } else if ( rando > 0.2 ) {
  • error.randomValue = "low";
  • throw( error );
  • }
  • // NOTE: If we made it this far, there was no need to throw.
  • } catch ( caughtError ) {
  • console.error( caughtError );
  • }

As you can see, we're creating an Error() object on line-6; however, we're not throwing it until later on in the control-flow, after it has been augmented based on various conditional checks. As such, when we run this code, we [randomly] get the following terminal output:


 Error stack-traces in Node.js point to the line on which the Error() constructor was called - not on which the error was thrown. 

As you can see, the stack-trace (error.stack) points to line-6, which is where the Error() object was constructed. What we can't see very clearly in the stack-trace was which condition caused the error to finally be thrown. In this case, we happen to see the augmented property, "randomValue", showing a value of "low" which coincidentally gives us some insight. But, if that property weren't being output (such as in a case where a log-item serializer didn't look for non-standard error properties), then this stack-trace would be substantially less useful.

Ultimately, it is best to be instantiating and throwing errors in the same expression such that the line-number in the stack-trace actually points to the line on which the error is thrown. And, if you need to augment the error object properties, don't do it after the Error object is created - create a custom error object that extends the core Error and provides for more constructor arguments. Not only will this make the stack-traces more useful, it will also make the shape of the various error objects more predictable (which is a necessity in TypeScript and ts-node).

NOTE: In recent versions of Node.js that support ES6, you can create a class that simply "extends" the native Error object, making the sub-classing of Error a piece of delicious cake.

Reader Comments

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.