Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Seth Bienek
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Seth Bienek@sethbienek )

Creating Custom Error Objects In Node.js With Error.captureStackTrace()

By Ben Nadel on

Coming from the world of ColdFusion, I'm used to using the CFThrow tag (and throw() function), which allows me to throw error objects with a good deal of contextual information that can later be used for debugging purposes. As such, I wanted to see if I could create a custom Error class in my Node.js code that would mimic [some of] the properties available on the ColdFusion error object.


 
 
 

 
 
 
 
 

As I've been digging around though lots of example Node.js code, I've seen two different approaches to this problem: Create many Error sub-classes, one for each type of error. And, creating one type of flexible error sub-class. Personally, I don't see the value in having lots of different types of error objects - JavaScript, as a language, doesn't seem to cater to Constructor-based error-catching. As such, differentiating on an object property seems far easier than differentiating on a Constructor type.

Furthermore, with CFThrow, I'm used to differentiating based on the Type property; so, that's what I'll be exploring here, in a Node.js context.

In addition to custom error properties (such as message and detail), the real focal point of the error object is the stacktrace. In the V8 engine, the stacktrace of an error is gathered using the Error.captureStackTrace() method:

Error.captureStackTrace( errorObject, localContextFunction )

This method injects a "stack" property into the first argument and, optionally, excludes the localContextFunction from the stacktrace. So, for example, if we were to generate the stacktrace inside of an error Factory function, we could tell V8 to exclude the factory function when generating the stack. This would reduce the noise of the error implementation and confine the stacktrace to meaningful information about the error context.

In my exploration, I'm creating an app-error module that exports both the AppError() constructor as well as a createAppError() factory function. Since my error objects can be produced in two different ways, I'm passing an optional "localContextFunction" argument into my AppError() constructor. This way, if the error is produced by the factory function, I can still trim the stacktrace appropriately.

  • // Require our core node modules.
  • var util = require( "util" );
  •  
  • // Export the constructor function.
  • exports.AppError = AppError;
  •  
  • // Export the factory function for the custom error object. The factory function lets
  • // the calling context create new AppError instances without calling the [new] keyword.
  • exports.createAppError = createAppError;
  •  
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  •  
  • // I create the new instance of the AppError object, ensureing that it properly
  • // extends from the Error class.
  • function createAppError( settings ) {
  •  
  • // NOTE: We are overriding the "implementationContext" so that the createAppError()
  • // function is not part of the resulting stacktrace.
  • return( new AppError( settings, createAppError ) );
  •  
  • }
  •  
  •  
  • // I am the custom error object for the application. The settings is a hash of optional
  • // properties for the error instance:
  • // --
  • // * type: I am the type of error being thrown.
  • // * message: I am the reason the error is being thrown.
  • // * detail: I am an explanation of the error.
  • // * extendedInfo: I am additional information about the error context.
  • // * errorCode: I am a custom error code associated with this type of error.
  • // --
  • // The implementationContext argument is an optional argument that can be used to trim
  • // the generated stacktrace. If not provided, it defaults to AppError.
  • function AppError( settings, implementationContext ) {
  •  
  • // Ensure that settings exists to prevent refernce errors.
  • settings = ( settings || {} );
  •  
  • // Override the default name property (Error). This is basically zero value-add.
  • this.name = "AppError";
  •  
  • // Since I am used to ColdFusion, I am modeling the custom error structure on the
  • // CFThrow functionality. Each of the following properties can be optionally passed-in
  • // as part of the Settings argument.
  • // --
  • // See CFThrow documentation: https://wikidocs.adobe.com/wiki/display/coldfusionen/cfthrow
  • this.type = ( settings.type || "Application" );
  • this.message = ( settings.message || "An error occurred." );
  • this.detail = ( settings.detail || "" );
  • this.extendedInfo = ( settings.extendedInfo || "" );
  • this.errorCode = ( settings.errorCode || "" );
  •  
  • // This is just a flag that will indicate if the error is a custom AppError. If this
  • // is not an AppError, this property will be undefined, which is a Falsey.
  • this.isAppError = true;
  •  
  • // Capture the current stacktrace and store it in the property "this.stack". By
  • // providing the implementationContext argument, we will remove the current
  • // constructor (or the optional factory function) line-item from the stacktrace; this
  • // is good because it will reduce the implementation noise in the stack property.
  • // --
  • // Rad More: https://code.google.com/p/v8-wiki/wiki/JavaScriptStackTraceApi#Stack_trace_collection_for_custom_exceptions
  • Error.captureStackTrace( this, ( implementationContext || AppError ) );
  •  
  • }
  •  
  • util.inherits( AppError, Error );

Personally, I'd rather not see the "new" keyword in the calling context as I think it will make the code harder to read. As such, I'm more likely to use the error factory function rather than the AppError() constructor directly. To see this in action, I've created a small demo that will throw a custom application error:

  • // Require our core node modules.
  • var util = require( "util" );
  •  
  • // Require our application node modules.
  • // --
  • // NOTE: I am renaming the createAppError() factory function to be appError(). I think
  • // this just makes the code a bit easier to read.
  • var appError = require( "./app-error" ).createAppError;
  •  
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  •  
  • // Try to call some code we know will throw an error.
  • try {
  •  
  • thisMethod();
  •  
  • // Output our custom error instance.
  • } catch ( error ) {
  •  
  • console.log( error.stack );
  • console.log( "Type: " + error.type );
  • console.log( "Message: " + error.message );
  • console.log( "Detail: " + error.detail );
  • console.log( "Extended Info: " + error.extendedInfo );
  • console.log( "Error Code: " + error.errorCode );
  •  
  • }
  •  
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  •  
  • // I throw a custom app error.
  • function thatMethod() {
  •  
  • throw(
  • appError({
  • type: "App.MissingArgument",
  • message: "You are missing an argument.",
  • detail: util.format( "The argument [%s] is required but was not passed-in.", "foo" ),
  • extendedInfo: "No! No weezing the joo-ooce!"
  • })
  • );
  •  
  • }
  •  
  •  
  • // I am here just to show nested call-stacks in the stacktrace.
  • function thisMethod() {
  •  
  • thatMethod();
  •  
  • }

As you can see, I'm throwing an error using the createAppError() factory function (which I've renamed as appError() in the test code). When we run this code, I get the following console output:

AppError: You are missing an argument.
- at thatMethod (/..../nodejs/custom-errors/test.js:42:3)
- at thisMethod (/..../nodejs/custom-errors/test.js:56:2)
- at Object.<anonymous> (/..../nodejs/custom-errors/test.js:19:2)
- at Module._compile (module.js:460:26)
- at Object.Module._extensions..js (module.js:478:10)
- at Module.load (module.js:355:32)
- at Function.Module._load (module.js:310:12)
- at Function.Module.runMain (module.js:501:10)
- at startup (node.js:129:16)
- at node.js:814:3
Type: App.MissingArgument
Message: You are missing an argument.
Detail: The argument [foo] is required but was not passed-in.
Extended Info: No! No weezing the joo-ooce!
Error Code:

As you can see, I am able to get the stacktrace of the error, excluding any of the details within the AppError() implementation. And, while it's not part of the initial console.log() output, I can easily access my additional error properties directly on the error object.

Once I have this kind of error object, I can start to manage my errors based on the exposed type property:

  • try {
  •  
  • // ... some code that may throw an error.
  •  
  • } catch ( error ) {
  •  
  • switch ( error.type ) {
  • case "App.ThisError":
  •  
  • // ... handle this error.
  •  
  • break;
  • case "App.ThatError":
  •  
  • // ... handle that error.
  •  
  • break;
  • case "App.OtherError":
  •  
  • // ... handle other error.
  •  
  • break;
  • default:
  •  
  • // ... Hmm, unexpected error, rethrow it... or maybe do something
  • // else with it, like return a rejected promise.
  • throw( error );
  •  
  • break;
  • }
  •  
  • }

Coming from a ColdFusion background, this looks very comfortable and familiar to me. But, I am very new to Node.js, so your mileage may vary. More than anything, however, I love the idea of being able to add a bunch of debugging information directly to the error object itself. Once we get into asynchronous code and promises and event loops (oh my!), the stacktrace will likely be less useful. As such, I'd like my error-handling code to be able to emphasize custom error properties and downplay a deep stack.




Reader Comments

Thanks for sharing your approach with error management in NodeJs. It's seems to be a very controversial topic with a lot of different approaches. I personally like yours because it offers a nice interface to both create a new exception and to handle it.

Reply to this Comment

@Tangxinfa,

Very interesting - I had not seen that before, will have to look into it. At first glance, it seems like a similar idea, but with some more structure applied to it.

Reply to this Comment

@Luciano,

Much appreciated! By using the object-based arguments, I find it really easy to see the key-value pairs much more clearly than if I was trying to use index-based arguments. Plus, this approach makes having optional arguments much easier since they don't have to rely on ordering.

Reply to this Comment

Great share. Unlike other Error overrides and inheritances, this works with Bluebird promises - qualifies as a bonafide error object.

Reply to this Comment

Thanks for sharing.

But what about the asynchronous error/exceptions handling?
I'm searching some example on how to deal with this kind of errors in a simple way..

Reply to this Comment

You came from a ColdFusion background? I don't know if id ever admit that... lol. ColdFusion Developers are on the same level as something like.. Lego Engineers. lol

But great article! I'm creating an application that will have a couple custom exceptions, so this helps a lot.

Reply to this Comment

Thanks for the post its cool.
But at the moment I have the same principle you have applied here but want to extend it a bit more by adding an inheritance from appError. To be a little more clearer I'll make an example:
I want to have this kind of hierarchy {
Error -> AppError -> CustomError1
Error -> AppError -> CustomError2
Error -> AppError -> CustomError3
}
I've tried applying it with the utils.inherits(appError, customError1), but cant seem to get access or even set the native properties of appError. To make an example lets say I have:
appError.a, appError.b, appError.c
and customError.x and customError.z

I only seem to set and get "x" and "z" but no success on getting to "a","b" or "c"

Think you can help out with this, and thanks again for the post

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
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.