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

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.

Tweet This Fascinating post by @BenNadel - Creating Custom Error Objects In Node.js With Error.captureStackTrace() Woot woot — you rock the party that rocks the body!







