A String is not an Error

  devthought.com        2011-12-23 08:00:32       3,637        0    

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:

  1. // A:
  2. function myFunction () {
  3.   if (somethingWrong) {
  4.     throw 'This is my error'
  5.   }
  6.   return allGood;
  7. }

and

  1. // B: async Node.JS-style callback with signature `fn(err, …)`
  2. function myFunction (callback) {
  3.   doSomethingAsync(function () {
  4.     // …
  5.     if (somethingWrong) {
  6.       callback('This is my error')
  7.     } else {
  8.       callback(null, …);
  9.     }
  10.   });
  11. }

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:

  1. // error.js
  2. var err = new Error();
  3. console.log(typeof err.stack);
  4. console.log(err.stack);
  1. ∞ node error.js
  2. string
  3. Error
  4.     at Object.<anonymous> (/private/tmp/error.js:2:11)
  5.     at Module._compile (module.js:411:26)
  6.     at Object..js (module.js:417:10)
  7.     at Module.load (module.js:343:31)
  8.     at Function._load (module.js:302:12)
  9.     at Array.0 (module.js:430:10)
  10.     at EventEmitter._tickCallback (node.js:126:26)

As you can see, even without throwing 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.

  1. Error.stackTraceLimit = 0; // disables it
  2. Error.stackTraceLimit = Infinity; // disables any limit

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

  1. function MongooseError (msg) {
  2.   Error.call(this);
  3.   Error.captureStackTrace(this, arguments.callee);
  4.   this.message = msg;
  5.   this.name = 'MongooseError';
  6. };

  7. MongooseError.prototype.__proto__ = Error.prototype;

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 CallSites 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)

  1. function a () {
  2.   b();
  3. }

  4. function b () {
  5.   var err = new Error;

  6.   Error.prepareStackTrace = function (err, stack) {
  7.     return stack;
  8.   };

  9.   Error.captureStackTrace(err, b);

  10.   err.stack.forEach(function (frame) {
  11.     console.error(' call: %s:%d - %s'
  12.       , frame.getFileName()
  13.       , frame.getLineNumber()
  14.       , frame.getFunctionName());
  15.   });
  16. }

  17. a();

This is an example of the output this script produces:

  1. ∞ node error-2.js
  2. call: /private/tmp/error-2.js:3 - a
  3. call: /private/tmp/error-2.js:23 -
  4. call: module.js:432 - Module._compile
  5. call: module.js:450 - Module._extensions..js
  6. call: module.js:351 - Module.load
  7. call: module.js:310 - Module._load
  8. call: module.js:470 - Module.runMain
  9. call: node.js:192 - startup.processNextTick.process._tickCallback

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.

  1. var currentFunction = arguments.callee.caller;
  2. while (currentFunction) {
  3.   var fn = currentFunction.toString();
  4.   var fname = fn.substring(fn.indexOf(&amp;quot;function&amp;quot;) + 8, fn.indexOf('')) || 'anonymous';
  5.   callstack.push(fname);
  6.   currentFunction = currentFunction.caller;
  7. }

Conclusions

  • When doing async I/O (in Node.JS or otherwise), since throwing is not a
    possibility (as it results in uncaught exceptions), Errors 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
    Errors. 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:

  1. // A:
  2. function myFunction () {
  3.   if (somethingWrong) {
  4.     throw new Error('This is my error')
  5.   }
  6.   return allGood;
  7. }

and

  1. // B: async Node.JS-style callback with signature `fn(err, …)`
  2. function myFunction (callback) {
  3.   doSomethingAsync(function () {
  4.     // …
  5.     if (somethingWrong) {
  6.       callback(new Error('This is my error'))
  7.     } else {
  8.       callback(null, …);
  9.     }
  10.   });
  11. }

Source : http://www.devthought.com/2011/12/22/a-string-is-not-an-error/

JAVASCRIPT  NODE.JS  STRING  ERROR OBJECT 

       

  RELATED


  0 COMMENT


No comment for this article.



  RANDOM FUN

Breaking working feature