index.js

const getPrototypesNames = obj => {
    const MAX_DEPTH = 100;
    const result = [];

    let i, currentPrototype;

    i = 0;
    currentPrototype = obj;

    while (currentPrototype && i++ < MAX_DEPTH) {
        const proto = Object.getPrototypeOf(currentPrototype);
        if (proto && proto.name) {
            const repeatedPrototypeName = result[result.length - 1] === proto.name;
            if (!repeatedPrototypeName) { // hide side effect of this library
                result.push(proto.name);
            }
        }
        currentPrototype = proto;
    }
    return result;
};

const getMessage = (error, message) => {
    return message || (error && error.message) || "";
};

const getDetails = (error, details) => {
    return details || (error && error.details) || null;
};

const MAX_ARGUMENTS = 3;

const parseArguments = (...args) => { // eslint-disable-line max-statements
    let sourceError, message, details;
    const invalidArguments = () => {
        throw new TypeError("Invalid arguments passed into error");
    };

    if (args.length > MAX_ARGUMENTS) {
        invalidArguments();
    }

    const useArgs = args.filter(a => a != null); // get rid of nulls and undefined

    const errors = useArgs.filter(a => a instanceof Error);
    const errorsLength = errors.length;
    if (errorsLength > 1) {
        invalidArguments();
    }

    const strings = useArgs.filter(a => typeof a === "string");
    const stringsLength = strings.length;
    if (stringsLength > 1) {
        invalidArguments();
    }

    const objects = useArgs.filter(a => typeof a === "object" && !(a instanceof Error));
    const objectsLength = objects.length;
    if (objectsLength > 1) {
        invalidArguments();
    }

    if (errorsLength) {
        sourceError = errors[0]; // accessing non-existing property is slower than this if
    }
    if (stringsLength) {
        message = strings[0];
    }
    if (objectsLength) {
        details = objects[0];
    }

    return {
        sourceError, message, details,
    };
};

const enhanceToString = array => {
    Object.defineProperty(array, "toString", {
        configurable: true,
        enumerable: false,
        value: function toStringCustom() { // eslint-disable-line no-param-reassign
            return "(" + Array.prototype.toString.call(this) + ")";
        },
        writable: true,
    });
    return array;
};

/**
 * @param {string} stack
 * @param {string} name
 * @param {string} message
 * @param {ErrorOptions} options
 * @private
 * @returns {string} new stack trace
 */
const cleanUpStack = (stack, name, message, { cleanStackTraces }) => {
    let newLines;

    const lines = stack.split("\n");
    const newFirstLine = name + (message ? ": " + message : "");

    lines.shift(); // remove name/message line
    lines.shift(); // remove first line of stack as it contains internal call we don't need
    if (cleanStackTraces) {
        newLines = lines.filter((line) => {
            return !(/at .* \(internal.*:\d+:\d+\)/.test(line));
        });
    }
    else {
        newLines = lines;
    }

    return newFirstLine + "\n" + newLines.join("\n");
};

/**
 * @typedef {Object} ErrorOptions
 * @property {boolean} cleanStackTraces - should stack trace be cleaned up from node internals
 */

/**
 * @typedef {Error} CustomError
 * @property {string} name - error name
 * @property {string} message - error message
 * @property {string} stack - error stack trace
 * @property {Object} details - error details
 * @property {Array.<Array|string>} names - hierarchy of extended/parent error names
 */

const defaultOptions = {
    cleanStackTraces: true,
};

/**
 * Creates new custom Error constructor.
 *
 * @param {string} name - error name
 * @param {Error} ParentError - Error to inherit from (built-in or custom error)
 * @param {ErrorOptions} options - options to override global options
 * @returns {CustomError}
 */
const createError = (name, ParentError = Error, options) => { // eslint-disable-line max-lines-per-function
    const useOptions = Object.assign( // eslint-disable-line prefer-object-spread
        {},
        defaultOptions,
        createError.defaultOptions,
        options,
    );

    /**
     * Creates custom error object instance. Arguments order doesn't matter.
     *
     * @param {Error|Object|string|void|null} arg1 - parent error instance or error message or error details object
     * @param {Error|Object|string|void|null} arg2 - parent error instance or error message or error details object
     * @param {Error|Object|string|void|null} arg3 - parent error instance or error message or error details object
     * @returns {CustomError}
     * @constructor
     */
    const CustomError = function CustomError(arg1, arg2, arg3) {
        if (!(this instanceof CustomError)) {
            return new CustomError(arg1, arg2, arg3);
        }

        const { sourceError, message, details } = parseArguments(arg1, arg2, arg3);

        const names = enhanceToString([name]);
        if (sourceError) {
            if (sourceError.names) {
                names.push(sourceError.names);
            }
            else {
                names.push(enhanceToString(getPrototypesNames(sourceError)));
            }
        }
        names.push(...getPrototypesNames(this));

        const useMessage = getMessage(sourceError, message);

        Object.defineProperties(this, {
            name: {
                configurable: true,
                enumerable: false,
                value: name,
                writable: true,
            },
            names: {
                configurable: true,
                enumerable: false,
                value: names,
                writable: true,
            },
            message: {
                configurable: true,
                enumerable: false,
                value: useMessage,
                writable: true,
            },
            details: {
                configurable: true,
                enumerable: false,
                value: getDetails(sourceError, details),
                writable: true,
            },
            stack: {
                configurable: true,
                enumerable: false,
                value: cleanUpStack(new Error().stack, name, useMessage, useOptions),
                writable: true,
            },
        });
    };

    CustomError.prototype = new ParentError();
    return CustomError;
};

export default createError;