Skip to content

Commit

Permalink
Refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Jun 5, 2022
1 parent 1c35bf3 commit 51b502b
Showing 1 changed file with 40 additions and 35 deletions.
75 changes: 40 additions & 35 deletions src/error/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand All @@ -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(
Expand All @@ -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`)
Expand All @@ -76,18 +54,15 @@ 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)
}

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
Expand All @@ -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
Expand All @@ -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, {
Expand Down

0 comments on commit 51b502b

Please sign in to comment.