@netflix/nerror
Version:
Rich errors
814 lines (729 loc) • 24.8 kB
JavaScript
'use strict';
/*
* verror.js: richer JavaScript errors
*/
const util = require('util');
const _ = require('lodash');
const assert = require('assert-plus');
const { sprintf } = require('extsprintf');
/*
* Public interface
*/
/* So you can 'var VError = require('@netflix/nerror')' */
module.exports = VError;
/* For compatibility */
VError.VError = VError;
/* Other exported classes */
VError.PError = PError;
VError.SError = SError;
VError.WError = WError;
VError.MultiError = MultiError;
/**
* Normalized forms, producing an object with the following properties.
* @private
* @typedef {Object} ParsedOptions parsed Options
* @param {Object} options e- quivalent to "options" in third form. This will
* never
* be a direct reference to what the caller passed in
* (i.e., it may be a shallow copy), so it can be freely
* modified.
* @param {String} shortmessage - result of sprintf(sprintf_args), taking
* `options.strict` into account as described in README.md.
*/
/**
* Common function used to parse constructor arguments for VError, WError, and
* SError. Named arguments to this function:
*
* strict force strict interpretation of sprintf arguments, even
* if the options in "argv" don't say so
*
* argv error's constructor arguments, which are to be
* interpreted as described in README.md. For quick
* reference, "argv" has one of the following forms:
*
* [ sprintf_args... ] (argv[0] is a string)
* [ cause, sprintf_args... ] (argv[0] is an Error)
* [ options, sprintf_args... ] (argv[0] is an object)
*
* @private
* @param {Array} args - arguments
* @returns {ParsedOptions} parsed options
*/
function parseConstructorArguments(args) {
let options, sprintf_args, shortmessage, k;
assert.object(args, 'args');
assert.bool(args.strict, 'args.strict');
assert.array(args.argv, 'args.argv');
assert.optionalBool(args.skipPrintf, 'args.skipPrintf');
const argv = args.argv;
/*
* First, figure out which form of invocation we've been given.
*/
if (argv.length === 0) {
options = {};
sprintf_args = [];
} else if (_.isError(argv[0])) {
options = { cause: argv[0] };
sprintf_args = argv.slice(1);
} else if (typeof argv[0] === 'object') {
options = {};
// eslint-disable-next-line guard-for-in
for (k in argv[0]) {
options[k] = argv[0][k];
}
sprintf_args = argv.slice(1);
} else {
assert.string(
argv[0],
'first argument to VError, PError, SError, or WError ' +
'constructor must be a string, object, or Error'
);
options = {};
sprintf_args = argv;
}
// Preserve options
if (args.skipPrintf) {
options.skipPrintf = args.skipPrintf;
}
if (args.strict) {
options.strict = args.strict;
}
/*
* Now construct the error's message.
*
* extsprintf (which we invoke here with our caller's arguments in order
* to construct this Error's message) is strict in its interpretation of
* values to be processed by the "%s" specifier. The value passed to
* extsprintf must actually be a string or something convertible to a
* String using .toString(). Passing other values (notably "null" and
* "undefined") is considered a programmer error. The assumption is
* that if you actually want to print the string "null" or "undefined",
* then that's easy to do that when you're calling extsprintf; on the
* other hand, if you did NOT want that (i.e., there's actually a bug
* where the program assumes some variable is non-null and tries to
* print it, which might happen when constructing a packet or file in
* some specific format), then it's better to stop immediately than
* produce bogus output.
*
* However, sometimes the bug is only in the code calling VError, and a
* programmer might prefer to have the error message contain "null" or
* "undefined" rather than have the bug in the error path crash the
* program (making the first bug harder to identify). For that reason,
* by default VError converts "null" or "undefined" arguments to their
* string representations and passes those to extsprintf. Programmers
* desiring the strict behavior can use the SError class or pass the
* "strict" option to the VError constructor.
*/
assert.object(options);
if (!options.skipPrintf && !options.strict && !args.strict) {
sprintf_args = sprintf_args.map(function(a) {
return a === null ? 'null' : a === undefined ? 'undefined' : a;
});
}
if (sprintf_args.length === 0) {
shortmessage = '';
} else if (
options.skipPrintf ||
(sprintf_args.length === 1 && typeof sprintf_args[0] === 'string')
) {
assert.equal(
sprintf_args.length,
1,
'only one argument is allowed with options.skipPrintf'
);
shortmessage = sprintf_args[0];
} else {
shortmessage = sprintf.apply(null, sprintf_args);
}
return {
options: options,
shortmessage: shortmessage
};
}
/**
* @public
* @typedef {Object} VErrorOptions Options
* @param {String} name - Name of the error.
* @param {Error} [cause] - Indicates that the new error was caused by `cause`.
* @param {Boolean} [strict=false] - If true, then `null` and `undefined` values
* in `sprintf_args` are passed through to `sprintf()`
* @param {Function} [constructorOpt] -If specified, then the stack trace for
* this error ends at function `constructorOpt`.
* @param {Object} [info]- Specifies arbitrary informational properties.
* @param {Boolean} [skipPrintf=false] - If true, then `sprintf()` is not called
*/
/**
*
* About Constructor:
* All of these forms construct a new VError that behaves just like the built-in
* JavaScript `Error` class, with some additional methods described below.
*
* About Properties:
* For all of these classes except `PError`, the printf-style arguments passed to
* the constructor are processed with `sprintf()` to form a message.
* For `WError`, this becomes the complete `message` property. For `SError` and
* `VError`, this message is prepended to the message of the cause, if any
* (with a suitable separator), and the result becomes the `message` property.
*
* The `stack` property is managed entirely by the underlying JavaScript
* implementation. It's generally implemented using a getter function because
* constructing the human-readable stack trace is somewhat expensive.
*
* @public
* @class VError
* @param {...String|VErrorOptions|Error} [arg] - sprintf args, options or cause
* @param {...String} [args] - sprintf args
* @property {String} name - Programmatically-usable name of the error.
* @property {String} message - Human-readable summary of the failure.
* Programmatically-accessible details are provided through `VError.info(err)`
* class method.
* @property {String} stack - Human-readable stack trace where the Error was
* constructed.
* @example
* // This is the most general form. You can specify any supported options
* // including "cause" and "info") this way.</caption>
* new VError(options, sprintf_args...)
* @example
* // This is a useful shorthand when the only option you need is "cause".
* new VError(cause, sprintf_args...)
* @example
* // This is a useful shorthand when you don't need any options at all.
* new VError(sprintf_args...)
*/
function VError(...args) {
let obj, ctor, message, k;
/*
* This is a regrettable pattern, but JavaScript's built-in Error class
* is defined to work this way, so we allow the constructor to be called
* without "new".
*/
if (!(this instanceof VError)) {
obj = Object.create(VError.prototype);
VError.apply(obj, arguments);
return obj;
}
/*
* For convenience and backwards compatibility, we support several
* different calling forms. Normalize them here.
*/
const parsed = parseConstructorArguments({
argv: args,
strict: false
});
/*
* If we've been given a name, apply it now.
*/
if (parsed.options.name) {
assert.string(parsed.options.name, 'error\'s "name" must be a string');
this.name = parsed.options.name;
}
/*
* For debugging, we keep track of the original short message (attached
* this Error particularly) separately from the complete message (which
* includes the messages of our cause chain).
*/
this.jse_shortmsg = parsed.shortmessage;
message = parsed.shortmessage;
/*
* If we've been given a cause, record a reference to it and update our
* message appropriately.
*/
const cause = parsed.options.cause;
if (cause) {
VError._assertError(cause, '"cause" must be an Error');
this.jse_cause = cause;
if (!parsed.options.skipCauseMessage) {
message += ': ' + cause.message;
}
}
/*
* If we've been given an object with properties, shallow-copy that
* here. We don't want to use a deep copy in case there are non-plain
* objects here, but we don't want to use the original object in case
* the caller modifies it later.
*/
this.jse_info = {};
if (parsed.options.info) {
// eslint-disable-next-line guard-for-in
for (k in parsed.options.info) {
this.jse_info[k] = parsed.options.info[k];
}
}
this.message = message;
Error.call(this, message);
if (Error.captureStackTrace) {
ctor = parsed.options.constructorOpt || this.constructor;
Error.captureStackTrace(this, ctor);
}
return this;
}
util.inherits(VError, Error);
VError.prototype.name = 'VError';
/**
* Appends any keys/fields to the existing jse_info. this can stomp over any
* existing fields.
* @public
* @memberof VError.prototype
* @param {Object} obj source obj to assign fields from
* @return {Object} new info object
*/
VError.prototype.assignInfo = function ve_assignInfo(obj) {
assert.optionalObject(obj, 'obj');
return Object.assign(this.jse_info, obj);
};
/**
* Instance level convenience method vs using the static methods on VError.
* @public
* @memberof VError.prototype
* @return {Object} info object
*/
VError.prototype.info = function ve_info() {
return VError.info(this);
};
/**
* A string representing the VError.
* @public
* @memberof VError.prototype
* @return {String} string representation
*/
VError.prototype.toString = function ve_toString() {
let str =
(this.hasOwnProperty('name') && this.name) ||
this.constructor.name ||
this.constructor.prototype.name;
if (this.message) {
str += ': ' + this.message;
}
return str;
};
/**
* This method is provided for compatibility. New callers should use
* VError.cause() instead. That method also uses the saner `null` return value
* when there is no cause.
* @public
* @memberof VError.prototype
* @return {undefined|Error} Error cause if any
*/
VError.prototype.cause = function ve_cause() {
const cause = VError.cause(this);
return cause === null ? undefined : cause;
};
/*
* Static methods
*
* These class-level methods are provided so that callers can use them on
* instances of Errors that are not VErrors. New interfaces should be provided
* only using static methods to eliminate the class of programming mistake where
* people fail to check whether the Error object has the corresponding methods.
*/
/**
* @private
* @static
* @memberof VError
* @param {Error} err - error to assert
* @param {String} [msg] - optional message
* @returns {undefined} no return value
* @throws AssertationError - when input is not an error
*/
VError._assertError = function _assertError(err, msg) {
assert.optionalString(msg, 'msg');
const _msg = (msg || 'err must be an Error') + ` but got ${String(err)}`;
assert.ok(_.isError(err), _msg);
};
/**
* Checks if an error is a VError or VError sub-class.
*
* @public
* @static
* @memberof VError
* @param {Error} err - error
* @return {Boolean} is a VError or VError sub-class
*/
VError.isVError = function assignInfo(err) {
// We are checking on internals here instead of using
// `err instanceof VError` to being compatible with the original VError lib.
return err && err.hasOwnProperty('jse_info');
};
/**
* Appends any keys/fields to the `jse_info`. This can stomp over any existing
* fields.
*
* Note: This method is static because in this way we don't need to check on
* VError versions to be sure `assignInfo` method is supported.
*
* @public
* @static
* @memberof VError
* @param {Error} err - error
* @param {Object} obj - source obj to assign fields from
* @return {Object} new info object
*/
VError.assignInfo = function assignInfo(err, obj) {
VError._assertError(err);
assert.optionalObject(obj, 'obj');
if (!VError.isVError(err)) {
throw new TypeError('err must be an instance of VError');
}
return Object.assign(err.jse_info, obj);
};
/**
* Returns the next Error in the cause chain for `err`, or `null` if there is no
* next error. See the `cause` argument to the constructor.
* Errors can have arbitrarily long cause chains. You can walk the `cause`
* chain by invoking `VError.cause(err)` on each subsequent return value.
* If `err` is not a `VError`, the cause is `null`.
*
* @public
* @static
* @memberof VError
* @param {VError} err - error
* @return {undefined|Error} Error cause if any
*/
VError.cause = function cause(err) {
VError._assertError(err);
return _.isError(err.jse_cause) ? err.jse_cause : null;
};
/**
* Returns an object with all of the extra error information that's been
* associated with this Error and all of its causes. These are the properties
* passed in using the `info` option to the constructor. Properties not
* specified in the constructor for this Error are implicitly inherited from
* this error's cause.
*
* These properties are intended to provide programmatically-accessible metadata
* about the error. For an error that indicates a failure to resolve a DNS
* name, informational properties might include the DNS name to be resolved, or
* even the list of resolvers used to resolve it. The values of these
* properties should generally be plain objects (i.e., consisting only of null,
* undefined, numbers, booleans, strings, and objects and arrays containing only
* other plain objects).
*
* @public
* @static
* @memberof VError
* @param {VError} err - error
* @return {Object} info object
*/
VError.info = function info(err) {
let rv, k;
VError._assertError(err);
const cause = VError.cause(err);
if (cause !== null) {
rv = VError.info(cause);
} else {
rv = {};
}
if (typeof err.jse_info === 'object' && err.jse_info !== null) {
// eslint-disable-next-line guard-for-in
for (k in err.jse_info) {
rv[k] = err.jse_info[k];
}
}
return rv;
};
/**
* The `findCauseByName()` function traverses the cause chain for `err`, looking
* for an error whose `name` property matches the passed in `name` value. If no
* match is found, `null` is returned.
*
* If all you want is to know _whether_ there's a cause (and you don't care what
* it is), you can use `VError.hasCauseWithName(err, name)`.
*
* If a vanilla error or a non-VError error is passed in, then there is no cause
* chain to traverse. In this scenario, the function will check the `name`
* property of only `err`.
*
* @public
* @static
* @memberof VError
* @param {VError} err - error
* @param {String} name - name of cause Error
* @return {null|Error} cause if any
*/
VError.findCauseByName = function findCauseByName(err, name) {
let cause;
VError._assertError(err);
assert.string(name, 'name');
assert.ok(name.length > 0, 'name cannot be empty');
for (cause = err; cause !== null; cause = VError.cause(cause)) {
assert.ok(_.isError(cause));
if (cause.name === name) {
return cause;
}
}
return null;
};
/**
* Returns true if and only if `VError.findCauseByName(err, name)` would return
* a non-null value. This essentially determines whether `err` has any cause in
* its cause chain that has name `name`.
*
* @public
* @static
* @memberof VError
* @param {VError} err - error
* @param {String} name - name of cause Error
* @return {Boolean} has cause
*/
VError.hasCauseWithName = function hasCauseWithName(err, name) {
return VError.findCauseByName(err, name) !== null;
};
/**
* Returns a string containing the full stack trace, with all nested errors
* recursively reported as `'caused by:' + err.stack`.
*
* @public
* @static
* @memberof VError
* @param {VError} err - error
* @return {String} full stack trace
*/
VError.fullStack = function fullStack(err) {
VError._assertError(err);
const cause = VError.cause(err);
if (cause) {
return err.stack + '\ncaused by: ' + VError.fullStack(cause);
}
return err.stack;
};
/**
* Given an array of Error objects (possibly empty), return a single error
* representing the whole collection of errors. If the list has:
*
* * 0 elements, returns `null`
* * 1 element, returns the sole error
* * more than 1 element, returns a MultiError referencing the whole list
*
* This is useful for cases where an operation may produce any number of errors,
* and you ultimately want to implement the usual `callback(err)` pattern.
* You can accumulate the errors in an array and then invoke
* `callback(VError.errorFromList(errors))` when the operation is complete.
*
* @public
* @static
* @memberof VError
* @param {Array<Error>} errors - errors
* @return {null|Error|MultiError} single or multi error if any
*/
VError.errorFromList = function errorFromList(errors) {
assert.arrayOfObject(errors, 'errors');
if (errors.length === 0) {
return null;
}
errors.forEach(function(e) {
assert.ok(_.isError(e), 'all errors must be an Error');
});
if (errors.length === 1) {
return errors[0];
}
return new MultiError(errors);
};
/**
* Convenience function for iterating an error that may itself be a MultiError.
*
* In all cases, `err` must be an Error. If `err` is a MultiError, then `func`
* is invoked as `func(errorN)` for each of the underlying errors of the
* MultiError.
* If `err` is any other kind of error, `func` is invoked once as `func(err)`.
* In all cases, `func` is invoked synchronously.
*
* This is useful for cases where an operation may produce any number of
* warnings that may be encapsulated with a MultiError -- but may not be.
*
* This function does not iterate an error's cause chain.
*
* @public
* @static
* @memberof VError
* @param {Error} err - error
* @param {Function} func - iterator
* @return {undefined} no return value
*/
VError.errorForEach = function errorForEach(err, func) {
VError._assertError(err);
assert.func(func, 'func');
if (err.name === 'MultiError') {
err.errors().forEach(function iterError(e) {
func(e);
});
} else {
func(err);
}
};
/**
* PError is like VError, but the message is not run through printf-style
* templating.
*
* @public
* @class PError
* @extends VError
* @param {...String|VErrorOptions|Error} [arg] - sprintf args, options or cause
* @param {...String} [args] - sprintf args
*/
function PError(...args) {
let obj;
if (!(this instanceof PError)) {
obj = Object.create(PError.prototype);
PError.apply(obj, args);
return obj;
}
const parsed = parseConstructorArguments({
argv: args,
strict: false,
skipPrintf: true
});
VError.call(this, parsed.options, parsed.shortmessage);
return this;
}
util.inherits(PError, VError);
PError.prototype.name = 'PError';
/**
* SError is like VError, but stricter about types. You cannot pass "null" or
* "undefined" as string arguments to the formatter.
*
* @public
* @class SError
* @extends VError
* @param {...String|VErrorOptions|Error} [arg] - sprintf args, options or cause
* @param {...String} [args] - sprintf args
*/
function SError(...args) {
let obj;
if (!(this instanceof SError)) {
obj = Object.create(SError.prototype);
SError.apply(obj, arguments);
return obj;
}
const parsed = parseConstructorArguments({
argv: args,
strict: true
});
const options = parsed.options;
options.skipPrintf = false;
VError.call(this, options, '%s', parsed.shortmessage);
return this;
}
/*
* We don't bother setting SError.prototype.name because once constructed,
* SErrors are just like VErrors.
*/
util.inherits(SError, VError);
/**
* Represents a collection of errors for the purpose of consumers that generally
* only deal with one error. Callers can extract the individual errors
* contained in this object, but may also just treat it as a normal single
* error, in which case a summary message will be printed.
*
* @public
* @class MultiError
* @extends VError
* @param {Array<Error>} errors - errors
* @example
* // `error_list` is an array of at least one `Error` object.
* new MultiError(error_list)
*
* // The cause of the MultiError is the first error provided. None of the
* // other `VError` options are supported. The `message` for a MultiError
* // consists the `message` from the first error, prepended with a message
* // indicating that there were other errors.
*
* //For example:
* err = new MultiError([
* new Error('failed to resolve DNS name "abc.example.com"'),
* new Error('failed to resolve DNS name "def.example.com"'),
* ]);
* console.error(err.message);
*
* // outputs:
* // first of 2 errors: failed to resolve DNS name "abc.example.com"
*/
function MultiError(errors) {
assert.array(errors, 'list of errors');
assert.ok(errors.length > 0, 'must be at least one error');
this.ase_errors = errors;
VError.call(
this,
{
cause: errors[0]
},
'first of %d error%s',
errors.length,
errors.length === 1 ? '' : 's'
);
}
util.inherits(MultiError, VError);
MultiError.prototype.name = 'MultiError';
/**
* Returns an array of the errors used to construct this MultiError.
*
* @public
* @memberof MultiError.prototype
* @returns {Array<Error>} errors
*/
MultiError.prototype.errors = function me_errors() {
return this.ase_errors.slice(0);
};
/**
* WError for wrapping errors while hiding the lower-level messages from the
* top-level error. This is useful for API endpoints where you don't want to
* expose internal error messages, but you still want to preserve the error
* chain for logging and debugging
*
* @public
* @class WError
* @extends VError
* @param {...String|VErrorOptions|Error} [arg] - sprintf args, options or cause
* @param {...String} [args] - sprintf args
*/
function WError(...args) {
let obj;
if (!(this instanceof WError)) {
obj = Object.create(WError.prototype);
WError.apply(obj, args);
return obj;
}
const parsed = parseConstructorArguments({
argv: args,
strict: false
});
const options = parsed.options;
options.skipCauseMessage = true;
options.skipPrintf = false;
VError.call(this, options, '%s', parsed.shortmessage);
return this;
}
util.inherits(WError, VError);
WError.prototype.name = 'WError';
/**
* A string representing the WError.
* @public
* @memberof WError.prototype
* @return {String} string representation
*/
WError.prototype.toString = function we_toString() {
let str =
(this.hasOwnProperty('name') && this.name) ||
this.constructor.name ||
this.constructor.prototype.name;
if (this.message) {
str += ': ' + this.message;
}
if (this.jse_cause && this.jse_cause.message) {
str += '; caused by ' + this.jse_cause.toString();
}
return str;
};
/**
* For purely historical reasons, WError's cause() function allows you to set
* the cause.
* @public
* @memberof WError.prototype
* @param {Error} c - cause
* @return {undefined|Error} Error cause
*/
WError.prototype.cause = function we_cause(c) {
if (_.isError(c)) {
this.jse_cause = c;
}
return this.jse_cause;
};