I decided to write a little article to discourage an unfortunately common pattern in Node.JS modules (and browser JavaScript, to a lesser extent) that can boil down to these two examples:
and
In both cases, passing a string instead of an error results in
reduced interoperability between modules. It breaks contracts with APIs
that might be performing instanceof Error
checks, or that want to know more about the error.
Error
objects, as we’ll see, have very interesting
properties in modern JavaScript engines besides holding the message
passed to the constructor.
The stack property
The fundamental benefit of Error objects is that they automatically keep track of where they were built and originated.
The mechanism of how this happens is specific to each JavaScript engine and/or browser that implements it.
V8 (Node.JS)
The way that V8 handles this, which directly affects us who develop in Node.JS is described by the StackTraceApi document.
We can test its behavior by simply initializing a Error
object and inspecting its stack
property:
As you can see, even without throw
ing or passing it around, V8 is able to tell us exactly where that object was created, and how it got there.
By default, V8 limits the stack trace size to 10 frames. You can alter this by changing the Error.stackTraceLimit
during runtime.
Custom Errors
If you wanted to extend the native Error
object so that stack collection is preserved, you can do so by calling the captureStackTrace
function. This is an example extracted from the Mongoose ODM
The second argument passed to the captureStackTrace
prevents unnecessary noise in the stack
generation by hiding the MongooseError
constructor calls from the stack.
Beyond stack strings
As you might have noticed in my previous code example, I purposedly printed the typeof
of the stack
property. As it turns out, it’s a regular String with a format optimized for readability.
V8, much like Java, allows complete runtime introspection of the stack. Instead of a string, we can access an array of CallSite
s that retain as much information as possible about the function call in the stack, including the object scope (this
).
In this example, we override the prepareStackTrace
function to access this raw array and examine it. We call `getFileName` and other methods on each CallSite
(all the available methods are described here)
This is an example of the output this script produces:
All this functionality would of course be non-existent if we were to pass around strings, therefore drastically shrinking our debugging panorama.
For real-world usage, restoring the original prepareStackTrace
after overriding it is probably a good idea. Thankfully, TJ Holowaychuck has released a tiny callsite module to make this painless.
Browsers
Like the V8 document states:
The API described here is specific to V8 and is not supported by any other JavaScript implementations. Most implementations do provide an error.stack property but the format of the stack trace is likely to be different from the format described here
- Firefox exposes
error.stack
. It has its own format. - Opera exposes
error.stacktrace
. It has its own format. - IE has no stack.
A very interesting solution to this is provided by javascript-stacktrace, which attempts to normalize a printed stack trace across all browsers.
On IE (and older versions of Safari), for example, it uses a clever method: it recursively looks for the caller
property of a function, calls toString
on it and parses out the function name.
Conclusions
- When doing async I/O (in Node.JS or otherwise), since
throw
ing is not a
possibility (as it results in uncaught exceptions),Error
s are the only way to
allow proper stack trace collection. - Even in non-V8 browser environments, it’s probably a good idea to still initialize
Error
s. The API exists in all browsers, and the extended API facilities that V8
provides are bound to be available to most engines in the future. - If you’re throwing or passing around strings for errors, consider switching today!
The examples in the beginning of the post can thus be rewritten this way:
and