error-ninja

A handy library for creating custom error constructors that work across async boundaries in Node.js. JavaScript error handling has never been this nice!

Usage no npm install needed!

<script type="module">
  import errorNinja from 'https://cdn.skypack.dev/error-ninja';
</script>

README

Error-Ninja

A handy library for creating custom error constructors that work across async boundaries in Node.js. JavaScript error handling has never been this nice!

What's this for?

This library is for you if you want to:

  1. Maintain error stack traces across async boundaries.
  2. Throw and catch errors to indicate problems instead of relying on function return values.
  3. Implement custom error constructors e.g. new DataStreamError().
  4. Attach data/properties to errors to give them additional context.
  5. Understand how bugs are triggered and how they bubble up your call stack.
  6. Work with native JavaScript errors as well as your own.
  7. Make your debugging life easier.
  8. Add an ID property to errors to make them easy to log and trace in production.
  9. Print out stack traces for uncaught errors and unhandled promise rejections.
  10. Add more power to your errors without altering their native JavaScript and Node.js functionality.

v1.x Breaking Changes

The new v1 of this library is a complete rewrite and is not backwards compatible with previous versions.

Quick Start

See the ./examples/example.js file for a working example or execute: npm run example.

const setupErrorNinja = require(`error-ninja`);

const createErrorClass = setupErrorNinja({
    stackTraceLimit: 20, // Expand the maximum number of stack trace frames for all errors.
    fullInsight: true, // Capture full errors and stack traces instead of just the error name, ID and message.
});

const DataStreamError = createErrorClass(`DataStreamError`);
const FatalError = createErrorClass(`FatalError`);

try {

    // ...something bad happens...

    throw new DataStreamError(`NETWORK_DISC`, `The network disconnected.`, { status: 500, uri: `...` });

}
catch (err) {
    const newErr = FatalError.chain(err, `DATA_INTERRUPTED`, `Unable to load the resource.`);
    console.error(newErr.pretty(true));
}

Example Code

Chaining errors together

The magic of ErrorNinja occurs when you chain errors together. This allows you to throw and catch as much as you like and obtain a full understanding of how errors propagate through your call stack.

const someError = new Error(`Something bad happened`);
const anotherError = DataStreamError.chain(someError, `DATA_INTERRUPTED`, `Failed to load the resource.`);
const newError = FatalError.chain(someError, `FATAL`, `Unable to continue.`);

Understanding the cause of an error

By default err.cause() returns an array of the IDs and messages for the chained errors up to this point. If true is passed as the first parameter and the fullInsight option is enabled it will return the original error objects.

const newErr = FatalError.chain(someError, `DATA_INTERRUPTED`, `Unable to load the resource.`);
const causes = newErr.cause();
causes.forEach(cause => console.error(`Cause:`, cause));

Pretty printing a chain of errors to the terminal

By default err.pretty() returns a formatted string containing the IDs and messages for the chained errors up to this point. If true is passed as the first parameter and the fullInsight option is enabled it will add the stack traces to the output.

const newErr = FatalError.chain(someError, `DATA_INTERRUPTED`, `Unable to load the resource.`);
const prettyOutput = newErr.pretty(true);
console.error(prettyOutput);

Example Output

Some sample output from the examples.js file.

Output when calling err.cause():

[ { name: 'FatalError',
    id: 'CRASHED',
    message: 'A fatal error occured.',
    data: { someProperty: 123 },
    __isLite: true },
  { name: 'VideoStreamError',
    id: 'CONNECT_FAILED',
    message: 'Unable to connect to the video server.',
    data: {},
    __isLite: true },
  { name: 'CustomCustomUriError',
    id: 'INVALID_URI',
    message: 'The specified URI is not a string.',
    data: { uri: null, typeOf: 'object' },
    __isLite: true } ]

Output when calling err.cause(true):

[ { FatalError: [CRASHED] A fatal error occured.
      at startVideoStream (/Users/josh/Repositories/Personal/Error-Ninja/examples/example.js:71:31)
      at process._tickCallback (internal/process/next_tick.js:68:7)
      at Function.Module.runMain (internal/modules/cjs/loader.js:744:11)
      at startup (internal/bootstrap/node.js:285:19)
      at bootstrapNodeJSCore (internal/bootstrap/node.js:739:3)
    __options: { fullInsight: true },
    name: 'FatalError',
    id: 'CRASHED',
    __originalMsg: 'A fatal error occured.',
    __chain: [ [NinjaError], [ErrorClass] ],
    __isErrorNinja: true,
    __isWrapped: false,
    __isLite: false,
    data: { someProperty: 123 } },
  { VideoStreamError: [CONNECT_FAILED] Unable to connect to the video server.
      at startVideoStream (/Users/josh/Repositories/Personal/Error-Ninja/examples/example.js:71:31)
      at process._tickCallback (internal/process/next_tick.js:68:7)
      at Function.Module.runMain (internal/modules/cjs/loader.js:744:11)
      at startup (internal/bootstrap/node.js:285:19)
      at bootstrapNodeJSCore (internal/bootstrap/node.js:739:3)
    __options: { fullInsight: true },
    name: 'VideoStreamError',
    id: 'CONNECT_FAILED',
    __originalMsg: 'Unable to connect to the video server.',
    __chain: [ [ErrorClass] ],
    __isErrorNinja: true,
    __isWrapped: false,
    __isLite: false,
    data: undefined },
  { CustomUriError: [INVALID_URI] The specified URI is not a string.
      at connectToVideoServer (/Users/josh/Repositories/Personal/Error-Ninja/examples/example.js:52:26)
      at startVideoStream (/Users/josh/Repositories/Personal/Error-Ninja/examples/example.js:66:9)
      at Object.<anonymous> (/Users/josh/Repositories/Personal/Error-Ninja/examples/example.js:97:1)
      at Module._compile (internal/modules/cjs/loader.js:688:30)
      at Object.Module._extensions..js (internal/modules/cjs/loader.js:699:10)
      at Module.load (internal/modules/cjs/loader.js:598:32)
      at tryModuleLoad (internal/modules/cjs/loader.js:537:12)
      at Function.Module._load (internal/modules/cjs/loader.js:529:3)
      at Function.Module.runMain (internal/modules/cjs/loader.js:741:12)
      at startup (internal/bootstrap/node.js:285:19)
      at bootstrapNodeJSCore (internal/bootstrap/node.js:739:3)
    __options: { fullInsight: true },
    name: 'CustomUriError',
    id: 'INVALID_URI',
    __originalMsg: 'The specified URI is not a string.',
    __chain: [],
    __isErrorNinja: true,
    __isWrapped: false,
    __isLite: false,
    data: { uri: null, typeOf: 'object' } } ]

Output when calling err.pretty():

===========================================================================
  <1> FatalError: [CRASHED] A fatal error occured.
---------------------------------------------------------------------------
  @property {number} someProperty: <123>
===========================================================================
  <2> VideoStreamError: [CONNECT_FAILED] Unable to connect to the video server.
===========================================================================
  <3> CustomUriError: [INVALID_URI] The specified URI is not a string.
---------------------------------------------------------------------------
  @property {object} uri: <null>
  @property {string} typeOf: <object>
===========================================================================

Output when calling err.pretty(true):

===========================================================================
  <1> FatalError: [CRASHED] A fatal error occured.
===========================================================================
  @property {number} someProperty: <123>
---------------------------------------------------------------------------
      at startVideoStream (/Users/josh/Repositories/Personal/Error-Ninja/examples/example.js:71:31)
      at process._tickCallback (internal/process/next_tick.js:68:7)
      at Function.Module.runMain (internal/modules/cjs/loader.js:744:11)
      at startup (internal/bootstrap/node.js:285:19)
      at bootstrapNodeJSCore (internal/bootstrap/node.js:739:3)
===========================================================================
  <2> VideoStreamError: [CONNECT_FAILED] Unable to connect to the video server.
---------------------------------------------------------------------------
      at startVideoStream (/Users/josh/Repositories/Personal/Error-Ninja/examples/example.js:71:31)
      at process._tickCallback (internal/process/next_tick.js:68:7)
      at Function.Module.runMain (internal/modules/cjs/loader.js:744:11)
      at startup (internal/bootstrap/node.js:285:19)
      at bootstrapNodeJSCore (internal/bootstrap/node.js:739:3)
===========================================================================
  <3> CustomUriError: [INVALID_URI] The specified URI is not a string.
===========================================================================
  @property {object} uri: <null>
  @property {string} typeOf: <object>
---------------------------------------------------------------------------
      at connectToVideoServer (/Users/josh/Repositories/Personal/Error-Ninja/examples/example.js:52:26)
      at startVideoStream (/Users/josh/Repositories/Personal/Error-Ninja/examples/example.js:66:9)
      at Object.<anonymous> (/Users/josh/Repositories/Personal/Error-Ninja/examples/example.js:97:1)
      at Module._compile (internal/modules/cjs/loader.js:688:30)
      at Object.Module._extensions..js (internal/modules/cjs/loader.js:699:10)
      at Module.load (internal/modules/cjs/loader.js:598:32)
      at tryModuleLoad (internal/modules/cjs/loader.js:537:12)
      at Function.Module._load (internal/modules/cjs/loader.js:529:3)
      at Function.Module.runMain (internal/modules/cjs/loader.js:741:12)
      at startup (internal/bootstrap/node.js:285:19)
      at bootstrapNodeJSCore (internal/bootstrap/node.js:739:3)
===========================================================================