Initial commit

master
Sven Slootweg 4 years ago
commit 7445225ce9

@ -0,0 +1,3 @@
{
"extends": "@joepie91/eslint-config"
}

1
.gitignore vendored

@ -0,0 +1 @@
node_modules

@ -0,0 +1,79 @@
# error-chain
Finally, a sensible way to deal with error handling in Javascript!
`error-chain` lets you create custom error types, that you can 'chain' to provide more context without losing the original error. It also provides a number of utilities for dealing with these 'chained errors', including tools for filtering error types.
__Work in progress!__ This is an experimental pre-release. All of the utilities are currently packaged into this single package to make iterating on the design easier, but they will eventually become separate packages under the `@error-chain/*` namespace.
While semantic versioning is still followed in principle, and so it should be fine to use this package in production already, it being a pre-release means there's a slightly higher chance of unintended API breakage, the documentation is not complete yet, and there may be bugs or awkward APIs. But if that's still an improvement over what you're currently doing - and for many people it probably will be - you should feel free to use it anyway!
__Library implementers:__ If you use `error-chain` in your library (which you should!), please make sure to describe in the README that you are using it, and link to the below section. That will help your users understand how to work with these sorts of errors. While they *can* be handled like normal `Error` objects, your users will be able to get much more information out of them with the right tools.
## If you are trying to work with or understand a chained error
You may be using a library that uses `error-chain`, and wonder what it's all about. Or maybe you got a pretty vague error, and it *seems* like there should be more information, but it's not quite visible. That's probably because the details are stored in what's called the "cause chain" - the chain of errors that led up to the one you're looking at.
This library provides a number of utilities to get the most out of these errors. These are:
- __getContext(error):__ This is a function that retrieves all of the "context" of every error along the chain, and merges it into a single object for you to use. "Context" is basically all of the custom properties on the errors, like HTTP status codes, protocol-specific error codes, and so on.
- __render(error):__ This is a function that, given an error (from `error-chain` or otherwise), produces a string that describes in detail what went wrong - it includes the original stacktrace from where the first error occurred, as well as a list of all the other errors in the chain, if any, to provide more context. For example, the source error may be "unable to open file", and a higher-level error might explain that this resulted in a failure to load the configuration. Note that this __does not__, by itself, log anything to the console - it just creates a string that you can log yourself, anywhere you want.
- __getChain(error):__ If you'd rather generate your own error display, or otherwise want to deal with the chain of errors, this function will give you the entire chain behind an error as an array, ordered from highest-level to lowest-level (source) error.
- __match(rules):__ If you're trying to write error handling code that only handles specific errors (which you should!), you can use this function to produce a 'filter' for them, based on your own rules - which get applied *to any error in the chain*. This means that if you try to `match(CustomErrorType)` for example, that will not only match top-level errors of the `CustomErrorType`, but any error that has a `CustomErrorType`-error *anywhere in its chain*. The API is explained more below.
- ... list to be completed ...
## API
### create(name, [options])
To be documented.
### match(rules)
Given one or more `rules`, this function will generate and return a so-called "predicate function" - ie. a function that takes an error as its argument, and then returns `true` or `false` depending on whether it matched. The `rules` are applied to every error in the chain, not just the top-level one - and if any of them matches, the predicate returns `true`.
In practice, you'll probably use this combined with something like Bluebird's `catch` filtering feature:
```js
doSomething().catch(match(HTTPError), (error) => /* ... */)
```
The `rules` themselves can be one of the following kinds of values:
- __An error type (constructor function or class):__ In this case, it will use `instanceof` on each error in the chain, to determine whether it's a match.
- __An object of properties:__ In this case, it will compare each error in the chain against the object, and consider it a match when all the (rule) properties match against the error. Only one error in the chain needs to match, but *all* of the properties need to match (excluding ones not specified in the rule).
- __A function:__ In this case, it will run the function *for each error in the chain*, and consider it a match if it returns `true` for any of them.
You can also specify *multiple* values in the `rules`, like so:
```js
match([ CustomErrorType, { errorCode: "OH_NO" } ])
```
In that case, an error will only be considered a match if *all* of the rules match against it, on the same error in the chain.
### is(error, rules)
Like `match(rules)`, but instead of creating a predicate function, it directly returns `true` or `false`. Useful if you want to manually check the error type/shape.
### rethrowAs(errorType, ... arguments)
To be documented.
### chain(error, errorType, ... arguments)
To be documented.
### getContext(error)
To be documented.
<!-- FIXME: Make sure to mention merge precedence -->
### getChain(error)
To be documented.
### render(error)
To be documented.

@ -0,0 +1,12 @@
"use strict";
module.exports = {
is: require("./src/packages/is"),
match: require("./src/packages/match"),
getChain: require("./src/packages/get-chain"),
getContext: require("./src/packages/get-context"),
create: require("./src/packages/create"),
chain: require("./src/packages/chain"),
rethrowAs: require("./src/packages/rethrow-as"),
render: require("./src/packages/render"),
};

@ -0,0 +1,5 @@
benefits of eTry
- exhaustive error matching
- you can wrap calls in it, as it's an expression, not a statement like try/catch
note that it does not work with async/await, where you're stuck with the manual approach

@ -0,0 +1,31 @@
{
"name": "error-chain",
"version": "0.1.0",
"main": "index.js",
"repository": "git@git.cryto.net:joepie91/node-error-chain.git",
"author": "Sven Slootweg <admin@cryto.net>",
"license": "WTFPL OR CC0-1.0",
"dependencies": {
"@validatem/allow-extra-properties": "^0.1.0",
"@validatem/core": "^0.3.10",
"@validatem/default-to": "^0.1.0",
"@validatem/dynamic": "^0.1.2",
"@validatem/error": "^1.1.0",
"@validatem/forbidden": "^0.1.0",
"@validatem/is-boolean": "^0.1.1",
"@validatem/is-function": "^0.1.0",
"@validatem/is-plain-object": "^0.1.1",
"@validatem/is-string": "^0.1.1",
"@validatem/one-of": "^0.1.1",
"@validatem/required": "^0.1.1",
"@validatem/wrap-error": "^0.1.3",
"chalk": "^2.4.2",
"fromentries": "^1.2.0",
"is.object": "^1.0.0",
"syncpipe": "^1.0.0"
},
"devDependencies": {
"@joepie91/eslint-config": "^1.1.0",
"eslint": "^7.4.0"
}
}

@ -0,0 +1,7 @@
"use strict";
module.exports = function chain(originalError, ErrorType, ... args) {
let error = new ErrorType(... args);
error.cause = originalError;
return error;
};

@ -0,0 +1,70 @@
"use strict";
const { validateArguments, validateValue } = require("@validatem/core");
const required = require("@validatem/required");
const isString = require("@validatem/is-string");
const dynamic = require("@validatem/dynamic");
const defaultTo = require("@validatem/default-to");
const isFunction = require("@validatem/is-function");
const forbidSpecialProperties = require("./validators/forbid-special-properties");
const isErrorConstructor = require("./validators/is-error-constructor");
const isContextSchema = require("./validators/is-context-schema");
module.exports = function createCustomErrorType(_name, _options) {
let [ name, options ] = validateArguments(arguments, {
name: [ required, isString ],
options: [ defaultTo({}), dynamic((options) => ({
inheritsFrom: [ isErrorConstructor, defaultTo.literal(Error) ],
context: [ forbidSpecialProperties, options.contextSchema ],
contextSchema: [ isContextSchema ],
populate: [ isFunction, defaultTo.literal((message, context) => {
return { message, context };
}) ]
}))]
});
// NOTE: Intentionally not using `function ErrorConstructor` since this mucks up the default stacktrace display.
let ErrorConstructor = function (... args) {
let populateResult = options.populate(...args);
let { message, context } = validateValue(populateResult, {
message: [ required, isString ],
context: [ forbidSpecialProperties, options.contextSchema, defaultTo({}) ]
});
// We want this to ideally show at the top of any debug/inspect output for the error object
this._help = "https://www.npmjs.com/package/error-chain";
// We cannot just `Error.call(this, message)` as that will result in the wrong error name in the .stack property
this.message = message;
// TODO: Apparently Safari doesn't have a stack at all? https://stackoverflow.com/a/5251506
if (typeof Error.captureStackTrace !== "undefined") {
Error.captureStackTrace(this, this.constructor);
} else {
// TODO: Figure out whether this has the same wrong-error-name issue
this.stack = (new Error()).stack;
}
ErrorConstructor._assignDefaultProperties(this);
Object.assign(this, context);
};
ErrorConstructor._assignDefaultProperties = function (errorObject) {
/* This is split out into a separate method, to make it possible to call it recursively up the parent error type tree, without invoking a full constructor for each. */
if (options.inheritsFrom._assignDefaultProperties != null) {
options.inheritsFrom._assignDefaultProperties(errorObject);
}
Object.assign(this, options.context);
};
ErrorConstructor.prototype = Object.create(options.inheritsFrom.prototype);
ErrorConstructor.prototype._errorChainError = true;
ErrorConstructor.prototype.name = name;
ErrorConstructor.prototype.constructor = ErrorConstructor;
return ErrorConstructor;
};

@ -0,0 +1,22 @@
"use strict";
const fromEntries = require("fromentries");
const syncpipe = require("syncpipe");
const wrapError = require("@validatem/wrap-error");
const forbidden = require("@validatem/forbidden");
const allowExtraProperties = require("@validatem/allow-extra-properties");
const reservedProperties = require("../../reserved-properties");
let forbidSpecialProperty = wrapError("Reserved property name cannot be used", forbidden);
module.exports = syncpipe(reservedProperties, [
(_) => _.filter((property) => property !== "constructor"),
(_) => _.map((property) => {
// NOTE: It is very important that `forbidSpecialProperty` below is wrapped in an array. Otherwise, for the `__proto__` property, it will actually treat the entire rules object as a validatem-special object due to its __proto__ being set directly to an object with the validatem-special marker... We're hacking around this by wrapping it in an array instead, which will not register as such. It's really a hack, though.
return [ property, [ forbidSpecialProperty ] ];
}),
(_) => fromEntries(_),
(_) => allowExtraProperties(_)
]);

@ -0,0 +1,14 @@
"use strict";
const isPlainObject = require("@validatem/is-plain-object");
// FIXME: Below currently does not support validating for special Validatem objects, need a validator for that
// let isContextSchema = anyProperty({
// key: [ required, isString ],
// value: [ required, either([
// isFunction,
// nestedArrayOf(isFunction)
// ])]
// });
module.exports = isPlainObject;

@ -0,0 +1,23 @@
"use strict";
// FIXME: Package separately under @validatem/
const isFunction = require("@validatem/is-function");
const ValidationError = require("@validatem/error");
module.exports = [
isFunction,
(func) => {
let current = func.prototype;
while (current != null) {
if (current === Error.prototype) {
return;
}
current = Object.getPrototypeOf(current);
}
throw new ValidationError(`Must be an error constructor`);
}
];

@ -0,0 +1,18 @@
"use strict";
module.exports = function getChain(error) {
let currentError = error;
let errors = [];
while (currentError != null) {
errors.push(currentError);
if (currentError._errorChainError === true) {
currentError = currentError.cause;
} else {
break;
}
}
return errors;
};

@ -0,0 +1,22 @@
"use strict";
const omitKeys = require("./omit-keys");
const reservedProperties = require("../reserved-properties");
const getChain = require("../get-chain");
let Context = Symbol("Context");
module.exports = function getContext(error) {
if (error[Context] != null) {
return error[Context];
} else {
let contexts = getChain(error)
.reverse()
.map((item) => omitKeys(item, reservedProperties));
let combinedContext = Object.assign({}, ... contexts);
error[Context] = combinedContext;
return combinedContext;
}
};

@ -0,0 +1,21 @@
"use strict";
const isObject = require("is.object");
module.exports = function omitKeys(object, keys) {
if (!isObject(object)) {
// FIXME: Use Validatem here
throw new Error("First argument must be an object");
} else {
let keySet = new Set(keys);
let newObject = {};
for (let key of Object.keys(object)) {
if (!keySet.has(key)) {
newObject[key] = object[key];
}
}
return newObject;
}
};

@ -0,0 +1,38 @@
"use strict";
const getChain = require("../get-chain");
function testRule(error, rule) {
if (typeof rule === "function") {
if (rule.prototype != null) {
// Constructor function / class
return error instanceof rule;
} else {
// Predicate function
return Boolean(rule(error));
}
} else if (typeof rule === "object") {
for (let [ key, value ] of Object.entries(rule)) {
if (error[key] !== value) {
return false;
}
}
return true;
} else {
throw new Error(`Invalid rule type`);
}
}
module.exports = function isErrorType(error, rules) {
// FIXME: Use validatem here as well, for rule validation?
let chain = getChain(error);
return chain.some((causeError) => {
if (Array.isArray(rules)) {
return rules.every((rule) => testRule(causeError, rule));
} else {
return testRule(causeError, rules);
}
});
};

@ -0,0 +1,9 @@
"use strict";
const is = require("../is");
module.exports = function match(rules) {
return function matchError(error) {
return is(error, rules);
};
};

@ -0,0 +1,82 @@
"use strict";
const chalk = require("chalk");
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const defaultTo = require("@validatem/default-to");
const isBoolean = require("@validatem/is-boolean");
const oneOf = require("@validatem/one-of");
const stripErrorFromStack = require("./strip-error-from-stack");
const isInstanceOf = require("./is-instance-of");
const getChain = require("../get-chain");
function formattedErrorHeading(error, colorAll = false) {
let formattedMessage = error.message.trim().replace(/\n/g, " \\n ");
let formattedName = chalk.red(`${chalk.bold(error.name)}:`);
let coloredMessage = (colorAll === true) ? chalk.red(formattedMessage) : formattedMessage;
return `${formattedName} ${coloredMessage}`;
// return chalk.red(`${chalk.bold(error.name)}: ${formattedMessage}`);
}
module.exports = function renderError(_error, _options) {
let [ error, options ] = validateArguments(arguments, {
error: [ required, isInstanceOf(Error) ],
options: [ defaultTo({}), {
// color: [ oneOf([ true, false, "auto" ]), defaultTo("auto") ], // FIXME: Implement
allStacktraces: [ isBoolean, defaultTo(false) ]
}]
});
let {allStacktraces} = options;
let errors = getChain(error);
let detailedErrorsToDisplay = (allStacktraces === true) ? errors : errors.slice(-1);
let summary = errors.map((error, i) => {
let prefix = (i > 0) ? "⤷ " : "";
/* IDEA: After every summarized error, add a summarized stacktrace; that is, a stacktrace that only contains the 'user code' entries that might point the developer at the source of the problem. To do that, we should filter out all node_modules and error-chain stuff, as well as internals like timers.js. Then, we should deduplicate lines across stacktraces, and hide the function name if it's boilerplate (eg. Promise.try.then stuff). Try this out with the 'rpm' regex in the CVM smartctl wrapper, as that covers all bases; duplication, stacktraces without user code, etc. */
// let cleanStack = error.stack.split("\n").filter((line) => {
// return (!line.includes("node_modules")
// && !line.includes("node-error-chain")
// && !line.includes("(timers.js")
// && !line.includes("<anonymous>")
// );
// }).join("\n");
// console.log(cleanStack);
return prefix + formattedErrorHeading(error);
}).join("\n");
let stacktraces = detailedErrorsToDisplay.map((error, i) => {
let causedByPrefix = (i > 0 ? "Caused by: " : "");
let causedByPadding = (i > 0) ? " " : "";
let strippedStack = stripErrorFromStack(error.stack);
let formattedStack = strippedStack.split("\n").map((line) => {
if (line.trim().length === 0) {
return null;
} else if (line[0] === " ") {
return causedByPadding + line;
} else {
return causedByPadding + ` ${line}`;
}
}).filter(line => line != null).join("\n");
let heading = causedByPadding + causedByPrefix + formattedErrorHeading(error);
return `${heading}\n${formattedStack}`;
}).join("\n\n");
let stacktraceSection = (allStacktraces === true)
? `${chalk.cyan("All stacktraces:")}\n\n${stacktraces}`
: `${chalk.cyan("Stacktrace for original error:")}\n\n${stacktraces}`;
return `${summary}\n\n${stacktraceSection}`;
};

@ -0,0 +1,17 @@
"use strict";
// FIXME: Package as @validatem/is-instance-of
const ValidationError = require("@validatem/error");
module.exports = function isInstanceOf(constructor) {
if (typeof constructor !== "function") {
throw new Error(`The constructor or class to validate against, must be a function`);
}
return function isInstanceOf(value) {
if (!(value instanceof constructor)) {
throw new ValidationError(`Must be an instance of ${constructor.name}`);
}
};
};

@ -0,0 +1,11 @@
"use strict";
module.exports = function stripErrorFromStack(stack) {
// FIXME: This will not work reliably in all browsers!
return stack
.split("\n")
.filter((line) => {
return /^\s*at .+\)$/.test(line);
})
.join("\n");
};

@ -0,0 +1,12 @@
"use strict";
module.exports = [
"_errorChainError",
"_help",
"name",
"message",
"stack",
"cause",
"__proto__",
"constructor"
];

@ -0,0 +1,9 @@
"use strict";
const chain = require("../chain");
module.exports = function rethrowAs(ErrorType, ... args) {
return function (error) {
throw chain(error, ErrorType, ... args);
};
};

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save