/notate

Extra err.stack entries for debuggability. Better than your plain 'if (err) { return cb(err) }'

Primary LanguageJavaScriptMIT LicenseMIT

@scottnonnenberg/notate

Making it easier to debug asynchronous errors in javascript. Better than if (err) { return cb(err) }

Blog post announcing this project: https://blog.scottnonnenberg.com/notate-better-javascript-callstacks/

continous integration code coverage npm version license

Quickstart

npm install --save-dev @scottnonnenberg/notate

In your code, whenever calling a callback, use notate to return the error instead of logging or just return cb():

import notate from '@scottnonnenberg/notate';

export function doAsync(url, cb) {
  someLib.go(url, function(err, result) {
    if (notate(cb, err, { url }) {
      return;
    }

    return cb(null, result);
  });
}

What does this get you?

  1. Now you know you won't crash the first time your code hits your error-handling code, due to a mis-typed return cb(err);. That return; statement is simple and easy to get right.
  2. Later, when that error bubbles up to your top level, you'll have an entry in the callstack for this function, added by notate(). The url property will also be added to the error to aid debugging. Note: if an url property already exists on the Error, it will be come url2.

You can use notate's prettyPrint() function to get the full benefit:

import { prettyPrint } from 'notate';
import doAsync from './do_async';

doAsync('http://somewhere.com', function(err, result) {
  if (err) {
    console.log(prettyPrint(err));
    process.exitCode = 1;
    return;
  }

  console.log(result);
})

And it will look like this on Node.js - see this page for browser examples):

{ [Error: Incorrect arguments supplied] url: ‘http://something’ }
    at **notate: /src/demos/4. Error from async call/b. with breadcrumbs.js:82:21
    at null._onTimeout (/src/demos/4. Error from async call/b. with breadcrumbs.js:13:17)
    at Timer.listOnTimeout (timers.js:92:15)

API

var notate = require('@scottnonnenberg/notate');
var justNotate = notate.justNotate;
var prettyPrint = notate.prettyPrint;

// or:
import { default as notate, justNotate, prettyPrint } from '@scottnonnenberg/notate';
  • notate(cb, err, data, level) (default) - give it your callback, potential error, and any data you'd like merged into the error for later debugging. Throws if cb is not a function. level can be used to capture a different callstack entry other than the immediate caller of this function.
  • justNotate(err, data, level) - just like notate() but without the cb parameter
  • prettyPrint(err, options) - takes an Error which has additional stack entries and additional data added to it and prints it to a string. Note that, for some browsers, we need to use a new alternateStack key to store the annotated stack. prettyPrint() will handle this properly. options takes these keys:
    • maxLines: maximum number of callstack lines to include (default: 10)

In-browser use

First, there's difficulty with minified code. The line numbers and function names won't mean much. However, the data you add to the error will still be present on the outputs of prettyPrint(), so I think it's worthwhile.

In development mode, you'll get an experience similar to the Node.js experience, except modern module loaders will put everything in one file. Naming your functions will really help here.

You can see what stacks look like on various browsers on the stack examples page.

Now, some notes on specific browsers:

PhantomJS

Sadly, the output on this platform you frequently use for testing is quite ugly. Its stack and stackArray fields are both large and enumerable, and are therefore included in the util.inspect() component of the prettyPrint().

But there's an additional weirdness with this browser - the stack property of Error objects neither writable nor configurable we can't annotate it! Thus, this library creates a second field to be annotated and then used in the prettyPrint(): err.alternateStack. It is writable and configurable, but not enumerable.

Internet Explorer 10/11

Everything will work as you expect, except for one thing: your Error objects will not have a callstack until you throw them. This library will annotate these stack-less Error objects, but you'll start from nothing.

To get the callstack when you create a new Error:

var err = new Error('something went wrong');
if (!err.stack) {
  try {
    throw err;
  }
  catch (e) {
    err = e;
  }
}

Total pain, yes. Edge doesn't have this problem.

Internet Explorer 9 and Safari 5

Sadly, these browsers don't give you stacktraces at all. So you're really down to the data you merge into the errors. Yes, you can use libraries like StackTrace.js, but I'm not willing to go that far. Maybe if you ask really nice. But you should probably just fork this project to support those scenarios.

Do be aware that this project's Babel-produced code won't even load in IE9 without some modifications to the use of certain keywords. The es3ify node module and its Webpack loader will be necessary.

Additionally, when working with these browsers I would recommend that you force IE into standards mode: http://stackoverflow.com/questions/10975107/forcing-internet-explorer-9-to-use-standards-document-mode

Internet Explorer 8 and below

Like IE9, old versions of IE won't provide callstacks. But you will find that modern Javascript techniques will cause parse and execute problems on these old browsers.

Thus, I used the babel plugin es2015-loose so basic modules don't use Object.defineProperty() to give proper ES2015 module semantics: http://www.2ality.com/2015/12/babel6-loose-mode.html In older browsers, Object.defineProperty() only works for DOM nodes. We didn't really need those semantics, right? :0)

Contributing

This project uses standard-version to release new versions, automatically updating the version number and changelog based on commit messages in standard format. ghooks and validate-commit-msg are used to ensure all commit messages match the expected format (see package.json for the configuration details).

It takes some getting used to, but this configuration is absolutely worthwhile. A changelog is way easier to understand than the chaos of a raw commit stream, especially with standard-version providing direct links to bugs, commits and commit ranges.

License

(The MIT license)

Copyright (c) 2016 Scott Nonnenberg scott@nonnenberg.com

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.