diff --git a/src/error/create.js b/src/error/create.js index 7c36954e..9be14745 100644 --- a/src/error/create.js +++ b/src/error/create.js @@ -5,8 +5,7 @@ export const createErrorType = function (name, onCreate = defaultOnCreate) { const ErrorType = class extends Error { constructor(message, opts = {}) { validateOpts(opts) - const errorOpts = opts.cause === undefined ? {} : { cause: opts.cause } - super(message, errorOpts) + super(message, getErrorOpts(opts)) // eslint-disable-next-line fp/no-this onCreate(this, getOnCreateOpts(this, opts)) } @@ -16,34 +15,8 @@ export const createErrorType = function (name, onCreate = defaultOnCreate) { return ErrorType } -// Validate `error.name` looks like `ExampleError`. -const validateErrorName = function (name) { - if (typeof name !== 'string') { - throw new TypeError(`Error name must be a string: ${name}`) - } - - if (!name.endsWith(ERROR_NAME_END) || name === ERROR_NAME_END) { - throw new Error(`Error name "${name}" must end with "${ERROR_NAME_END}"`) - } - - validateErrorNamePattern(name) -} - -const validateErrorNamePattern = function (errorName) { - if (errorName[0] !== errorName.toUpperCase()[0]) { - throw new Error( - `Error name "${errorName}" must start with an uppercase letter.`, - ) - } - - if (!ERROR_NAME_REGEXP.test(errorName)) { - throw new Error(`Error name "${errorName}" must only contain letters.`) - } -} - -const ERROR_NAME_END = 'Error' -const ERROR_NAME_REGEXP = /[A-Z][a-zA-Z]*Error$/u - +// Due to `error.cause`, the second argument should always be a plain object +// We enforce no third argument since this is cleaner. const validateOpts = function (opts) { if (typeof opts !== 'object' || opts === null) { throw new TypeError( @@ -52,6 +25,11 @@ const validateOpts = function (opts) { } } +// Passing `{ cause: undefined }` creates `error.cause`, unlike passing `{}` +const getErrorOpts = function ({ cause }) { + return cause === undefined ? {} : { cause } +} + // When passing options to `onCreate()`, ignore keys that: // - Would override `Object` prototype (`hasOwnProperty`, etc.) or `Error` // prototype (`toString`) @@ -76,6 +54,7 @@ const getOnCreateOpts = function (error, opts) { const { propertyIsEnumerable: isEnum } = Object.prototype +// Uses `key in error` to handle any current and future error|object properties const shouldIgnoreKey = function (error, key) { return key in error || IGNORED_KEYS.has(key) } @@ -83,11 +62,7 @@ const shouldIgnoreKey = function (error, key) { const IGNORED_KEYS = new Set(['errors', 'prototype']) // `onCreate(error, opts)` allows custom logic at initialization time. -// The construction arguments are passed. -// - They can be validated, normalized, etc. -// - No third argument is passed. This enforces calling named parameters -// `new ErrorType('message', opts)` instead of positional ones, which is -// cleaner. +// The construction `opts` are passed, i.e. can be validated, normalized, etc. // `onCreate()` is useful to assign error instance-specific properties. // - Therefore, the default value just assign `opts`. // Properties that are error type-specific (i.e. same for all instances of that @@ -110,6 +85,35 @@ const defaultOnCreate = function (error, opts) { Object.assign(error, opts) } +// Validate `error.name` looks like `ExampleError` for consistency with +// native error types and common practices that users might expect +const validateErrorName = function (name) { + if (typeof name !== 'string') { + throw new TypeError(`Error name must be a string: ${name}`) + } + + if (!name.endsWith(ERROR_NAME_END) || name === ERROR_NAME_END) { + throw new Error(`Error name "${name}" must end with "${ERROR_NAME_END}"`) + } + + validateErrorNamePattern(name) +} + +const validateErrorNamePattern = function (errorName) { + if (errorName[0] !== errorName.toUpperCase()[0]) { + throw new Error( + `Error name "${errorName}" must start with an uppercase letter.`, + ) + } + + if (!ERROR_NAME_REGEXP.test(errorName)) { + throw new Error(`Error name "${errorName}" must only contain letters.`) + } +} + +const ERROR_NAME_END = 'Error' +const ERROR_NAME_REGEXP = /[A-Z][a-zA-Z]*Error$/u + // To mimic native error types and to print correctly with `util.inspect()`: // - `error.name` should be assigned on the prototype, not on the instance // - the constructor `name` must be set too @@ -118,6 +122,7 @@ const setErrorName = function (ErrorType, name) { setNonEnumProp(ErrorType.prototype, 'name', name) } +// Ensure those properties are not enumerable const setNonEnumProp = function (object, propName, value) { // eslint-disable-next-line fp/no-mutating-methods Object.defineProperty(object, propName, {