包详细信息

modern-errors

ehmicky24kMIT7.1.2

Handle errors in a simple, stable, consistent way

nodejs, javascript, stacktrace, library

自述文件

<picture> <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ehmicky/design/main/modern-errors/modern-errors_dark.svg"/> modern-errors logo </picture>

Node Browsers TypeScript Codecov Minified size Mastodon Medium

Handle errors in a simple, stable, consistent way.

Features

Simple patterns to:

Stability:

Plugins

Example

Create error classes.

import ModernError from 'modern-errors'

export const BaseError = ModernError.subclass('BaseError')

export const UnknownError = BaseError.subclass('UnknownError')
export const InputError = BaseError.subclass('InputError')
export const AuthError = BaseError.subclass('AuthError')
export const DatabaseError = BaseError.subclass('DatabaseError')

Set error properties.

throw new InputError('Invalid file path', { props: { filePath: '/...' } })

Wrap errors.

try {
  // ...
} catch (cause) {
  throw new InputError('Could not read the file.', { cause })
}

Normalize errors.

try {
  throw 'Missing file path.'
} catch (error) {
  // Normalized from a string to a `BaseError` instance
  throw BaseError.normalize(error)
}

Use plugins.

import ModernError from 'modern-errors'
import modernErrorsSerialize from 'modern-errors-serialize'

export const BaseError = ModernError.subclass('BaseError', {
  plugins: [modernErrorsSerialize],
})

// ...

// Serialize error as JSON, then back to identical error instance
const error = new InputError('Missing file path.')
const errorString = JSON.stringify(error)
const identicalError = BaseError.parse(JSON.parse(errorString))

Install

npm install modern-errors

If any plugin is used, it must also be installed.

npm install modern-errors-{pluginName}

This package works in both Node.js >=18.18.0 and browsers.

This is an ES module. It must be loaded using an import or import() statement, not require(). If TypeScript is used, it must be configured to output ES modules, not CommonJS.

Usage

⛑️ Error classes

Create error classes

import ModernError from 'modern-errors'

export const BaseError = ModernError.subclass('BaseError')

export const UnknownError = BaseError.subclass('UnknownError')
export const InputError = BaseError.subclass('InputError')
export const AuthError = BaseError.subclass('AuthError')
export const DatabaseError = BaseError.subclass('DatabaseError')

Export error classes

Exporting and documenting all error classes allows consumers to check them. This also enables sharing error classes between modules.

Check error classes

if (error instanceof InputError) {
  // ...
}

Error subclasses

ErrorClass.subclass() returns a subclass. Parent classes' options are merged with their subclasses.

export const BaseError = ModernError.subclass('BaseError', {
  props: { isError: true },
})
export const InputError = BaseError.subclass('InputError', {
  props: { isUserError: true },
})

const error = new InputError('...')
console.log(error.isError) // true
console.log(error.isUserError) // true
console.log(error instanceof BaseError) // true
console.log(error instanceof InputError) // true

🏷️ Error properties

Error class properties

const InputError = BaseError.subclass('InputError', {
  props: { isUserError: true },
})
const error = new InputError('...')
console.log(error.isUserError) // true

Error instance properties

const error = new InputError('...', { props: { isUserError: true } })
console.log(error.isUserError) // true

Internal error properties

Error properties that are internal or secret can be prefixed with _. This makes them non-enumerable, which prevents iterating or logging them.

const error = new InputError('...', {
  props: { userId: 6, _isUserError: true },
})
console.log(error.userId) // 6
console.log(error._isUserError) // true
console.log(Object.keys(error)) // ['userId']
console.log(error) // `userId` is logged, but not `_isUserError`

🎀 Wrap errors

Throw errors

throw new InputError('Missing file path.')

Wrap inner error

Any error's message, class and options can be wrapped using the standard cause option.

Instead of being set as a cause property, the inner error is directly merged to the outer error, including its message, stack, name, AggregateError.errors and any additional property.

try {
  // ...
} catch (cause) {
  throw new InputError('Could not read the file.', { cause })
}

Wrap error message

The outer error message is appended, unless it is empty. If the outer error message ends with : or :\n, it is prepended instead.

const cause = new InputError('File does not exist.')
// InputError: File does not exist.
throw new InputError('', { cause })
// InputError: File does not exist.
// Could not read the file.
throw new InputError('Could not read the file.', { cause })
// InputError: Could not read the file: File does not exist.
throw new InputError(`Could not read the file:`, { cause })
// InputError: Could not read the file:
// File does not exist.
throw new InputError(`Could not read the file:\n`, { cause })

Wrap error class

The outer error's class replaces the inner one.

try {
  throw new AuthError('...')
} catch (cause) {
  // Now an InputError
  throw new InputError('...', { cause })
}

Except when the outer error's class is a parent class, such as BaseError.

try {
  throw new AuthError('...')
} catch (cause) {
  // Still an AuthError
  throw new BaseError('...', { cause })
}

Wrap error options

The outer error's props and plugin options are merged.

try {
  throw new AuthError('...', innerOptions)
} catch (cause) {
  // `outerOptions` are merged with `innerOptions`
  throw new BaseError('...', { ...outerOptions, cause })
}

Aggregate errors

The errors option aggregates multiple errors into one. This is like new AggregateError(errors) except that it works with any error class.

const databaseError = new DatabaseError('...')
const authError = new AuthError('...')
throw new InputError('...', { errors: [databaseError, authError] })
// InputError: ... {
//   [errors]: [
//     DatabaseError: ...
//     AuthError: ...
//   ]
// }

🚨 Normalize errors

Wrapped errors

Any error can be directly passed to the cause or errors option, even if it is invalid, unknown or not normalized.

try {
  // ...
} catch (cause) {
  throw new InputError('...', { cause })
}

Invalid errors

Manipulating errors that are not Error instances or that have invalid properties can lead to unexpected bugs. BaseError.normalize() fixes that.

try {
  throw 'Missing file path.'
} catch (invalidError) {
  // This fails: `invalidError.message` is `undefined`
  console.log(invalidError.message.trim())
}
try {
  throw 'Missing file path.'
} catch (invalidError) {
  const normalizedError = BaseError.normalize(invalidError)
  // This works: 'Missing file path.'
  // `normalizedError` is a `BaseError` instance.
  console.log(normalizedError.message.trim())
}

🐞 Unknown errors

Handling known errors

Known errors should be handled in a try {} catch {} block and wrapped with a specific class. That block should only cover the statement that might throw in order to prevent catching other unrelated errors.

try {
  return regExp.test(value)
} catch (error) {
  // Now an `InputError` instance
  throw new InputError('Invalid regular expression:', { cause: error })
}

Normalizing unknown errors

If an error is not handled as described above, it is considered unknown. This indicates an unexpected exception, usually a bug. BaseError.normalize(error, UnknownError) assigns the UnknownError class to those errors.

export const UnknownError = BaseError.subclass('UnknownError')
try {
  return regExp.test(value)
} catch (error) {
  // Now an `UnknownError` instance
  throw BaseError.normalize(error, UnknownError)
}

Top-level error handler

Wrapping a module's main functions with BaseError.normalize(error, UnknownError) ensures every error being thrown is valid, applies plugins, and has a class that is either known or UnknownError.

export const main = () => {
  try {
    // ...
  } catch (error) {
    throw BaseError.normalize(error, UnknownError)
  }
}

🔌 Plugins

List of plugins

Plugins extend modern-errors features. All available plugins are listed here.

Adding plugins

To use a plugin, please install it, then pass it to the plugins option.

npm install modern-errors-{pluginName}
import ModernError from 'modern-errors'

import modernErrorsBugs from 'modern-errors-bugs'
import modernErrorsSerialize from 'modern-errors-serialize'

export const BaseError = ModernError.subclass('BaseError', {
  plugins: [modernErrorsBugs, modernErrorsSerialize],
})
// ...

Custom plugins

Please see the following documentation to create your own plugin.

Plugin options

Most plugins can be configured with options. The option's name is the same as the plugin.

const options = {
  // `modern-errors-bugs` options
  bugs: 'https://github.com/my-name/my-project/issues',
  // `props` can be configured and modified like plugin options
  props: { userId: 5 },
}

Plugin options can apply to (in priority order):

export const BaseError = ModernError.subclass('BaseError', options)
export const InputError = BaseError.subclass('InputError', options)
throw new InputError('...', options)
  • A plugin method call: last argument, passing only that plugin's options
ErrorClass[methodName](...args, options[pluginName])
error[methodName](...args, options[pluginName])

🔧 Custom logic

The custom option can be used to provide an error class with additional methods, constructor, properties or options.

export const InputError = BaseError.subclass('InputError', {
  // The `class` must extend from the parent error class
  custom: class extends BaseError {
    // If a `constructor` is defined, its parameters must be (message, options)
    // Additional `options` can be defined.
    constructor(message, options) {
      message += options?.suffix ?? ''
      super(message, options)
    }

    isUserInput() {
      // ...
    }
  },
})

const error = new InputError('Wrong user name', { suffix: ': example' })
console.log(error.message) // 'Wrong user name: example'
console.log(error.isUserInput())

🤓 TypeScript

Please see the following documentation for information about TypeScript types.

API

ModernError

Top-level ErrorClass.

ErrorClass.subclass(name, options?)

name: string\ options: ClassOptions?

Creates and returns a child ErrorClass.

options

options.props

Type: object

Error class properties.

options.plugins

Type: Plugin[]

options.custom

Type: class extends ErrorClass {}

Custom class to add any methods, constructor or properties.

options.*

Any plugin options can also be specified.

new ErrorClass(message, options?)

message: string\ options: InstanceOptions?\ Return value: Error

options

options.props

Type: object

Error instance properties.

options.cause

Type: any

Inner error being wrapped.

options.errors

Type: any[]

Array of errors being aggregated.

options.*

Any plugin options can also be specified.

ErrorClass.normalize(error, NewErrorClass?)

error: Error | any\ NewErrorClass: subclass of ErrorClass\ Return value: Error

Normalizes invalid errors.

If the error's class is a subclass of ErrorClass, it is left as is. Otherwise, it is converted to NewErrorClass, which defaults to ErrorClass itself.

Modules

This framework brings together a collection of modules which can also be used individually:

Support

For any question, don't hesitate to submit an issue on GitHub.

Everyone is welcome regardless of personal background. We enforce a Code of conduct in order to promote a positive and inclusive environment.

Contributing

This project was made with ❤️. The simplest way to give back is by starring and sharing it online.

If the documentation is unclear or has a typo, please click on the page's Edit button (pencil icon) and suggest a correction.

If you would like to help us fix a bug or add a new feature, please check our guidelines. Pull requests are welcome!

ehmicky
ehmicky

💻 🎨 🤔 📖
const_var
const_var

🤔 💬
Andy Brenneke
Andy Brenneke

🤔 💬
Graham Fisher
Graham Fisher

🐛
renzor
renzor

💬 🤔
Eugene
Eugene

💻 🐛
Jonathan Chambers
Jonathan Chambers

⚠️ 🐛
heyhey123
heyhey123

🐛
Benjamin Kroeger
Benjamin Kroeger

🐛

更新日志

7.1.2

Bug fixes

  • When wrapping an error with a different class but the same error.name, do not repeat that name in the error message

7.1.1

Bug fixes

Documentation

7.1.0

Features

7.0.3

Documentation

  • Improve documentation in README.md

7.0.2

Bug fixes

7.0.1

Types

7.0.0

Breaking changes

  • Minimal supported Node.js version is now 18.18.0

6.0.0

Breaking changes

  • Minimal supported Node.js version is now 16.17.0

5.5.4

Bug fixes

5.5.3

Bug fixes

5.5.2

Bug fixes

5.5.1

Bug fixes

5.5.0

Features

  • Improve TypeScript types

5.4.0

Features

  • Improve TypeScript types

5.3.1

Bug fixes

  • Fix TypeScript types

5.3.0

Features

5.2.0

Documentation

  • Improve documentation

5.1.1

Bug fixes

  • Fix bug duplicating the stack trace when wrapping the error message

5.1.0

Features

5.0.0

Breaking changes

Top-level error class

The default export is now the top-level error class ModernError.

Also, the base error class is now documented as BaseError instead of AnyError.

Before:

import modernErrors from 'modern-errors'

export const AnyError = modernErrors(plugins, options)

After:

import ModernError from 'modern-errors'

export const BaseError = ModernError.subclass('BaseError', {
  ...options,
  plugins,
})

Error normalization

Creating an UnknownError class is now optional, although still recommended. To normalize unknown errors, UnknownError must now be explicitly passed as a second argument to BaseError.normalize().

Before:

export const main = () => {
  try {
    // ...
  } catch (error) {
    throw BaseError.normalize(error)
  }
}

After:

export const main = () => {
  try {
    // ...
  } catch (error) {
    throw BaseError.normalize(error, UnknownError)
  }
}

When UnknownError is not passed as a second argument, BaseError.normalize() now converts unknown errors to BaseError instances instead.

Before:

const error = new Error('example')
assert(BaseError.normalize(error) instanceof UnknownError)

After:

const error = new Error('example')
assert(BaseError.normalize(error) instanceof BaseError)
assert(!(BaseError.normalize(error) instanceof UnknownError))
assert(BaseError.normalize(error, UnknownError) instanceof UnknownError)

Wrap error options

When wrapping errors, the outer and inner error's options are now always merged.

Before:

try {
  throw new AuthError('...', innerOptions)
} catch (cause) {
  // Options are now `outerOptions`. `innerOptions` are discarded.
  throw new InputError('...', { ...outerOptions, cause })
}

After:

try {
  throw new AuthError('...', innerOptions)
} catch (cause) {
  // `outerOptions` are merged with `innerOptions`
  throw new InputError('...', { ...outerOptions, cause })
}

Wrap error class

When wrapping errors, the inner error's class is now kept if the outer error's class is a parent (including BaseError).

export const ParentError = BaseError.subclass('ParentError')
export const ChildError = ParentError.subclass('ChildError')

Before:

try {
  throw new ChildError('...')
} catch (cause) {
  // Now a ParentError
  throw new ParentError('...', { cause })
}

After:

try {
  throw new ChildError('...')
} catch (cause) {
  // Still a ChildError, because that is a subclass of ParentError
  throw new ParentError('...', { cause })
}

Aggregate errors

Aggregate errors must now be explicitly normalized by BaseError.normalize() instead of being automatically normalized on creation.

Features

Global custom logic

Global custom logic can now be specified by passing the custom option to the BaseError. Previously, only class-specific custom logic could be specified.

Class-specific plugins

Plugins can now be specific to an error class (and its subclasses) by using the plugins option. Previously plugins had to be applied to all error classes.

Optional wrapping

The BaseError can now be instantiated without wrapping an error. The cause option is now optional.

Missing stack trace

The stack trace produced when wrapping an error that does not have one has been improved.

Plugins

The following changes only impact authors of custom plugins.

info.ErrorClass

Static methods (including ErrorClass.normalize()) can now be called on any error class, not only on BaseError. As a consequence, info.AnyError has been renamed to info.ErrorClass.

info.ErrorClasses

info.ErrorClasses is now an array instead of an object. This array might contain error classes with duplicate names.

info.errorInfo

info.errorInfo(error) now returns the error's ErrorClass and ErrorClasses.

4.1.1

Bug fixes

  • Improve how stack traces are printed

4.1.0

Features

  • Add browser support

4.0.0

Major features

Plugins

Features can now be extended using plugins.

import modernErrors from 'modern-errors'

import modernErrorsBugs from 'modern-errors-bugs'
import modernErrorsCli from 'modern-errors-cli'

export const AnyError = modernErrors([modernErrorsBugs, modernErrorsCli])

CLI plugin

The modern-errors-cli plugin handles CLI errors.

Process errors

The modern-errors-process plugin handles process errors.

Clean stack traces

The modern-errors-stack plugin automatically cleans up stack traces.

HTTP responses

The modern-errors-http plugin converts errors to plain objects to use in an HTTP response.

Error logging (Winston)

The modern-errors-winston plugin logs errors with Winston.

Subclasses

Error subclasses can now be created using ErrorClass.subclass() to share custom logic and options between classes.

const SharedError = AnyError.subclass('SharedError', {
  custom: class extends AnyError {
    // ...
  },
})

export const InputError = SharedError.subclass('InputError')
export const AuthError = SharedError.subclass('AuthError')

Improved options

Options can now be applied to any error.

export const AnyError = modernErrors(plugins, options)

Or to any error of a specific class.

export const InputError = AnyError.subclass('InputError', options)

Or to multiple classes.

export const SharedError = AnyError.subclass('SharedError', options)

export const InputError = SharedError.subclass('InputError')
export const AuthError = SharedError.subclass('AuthError')

Or to a specific error.

throw new InputError('...', options)

Or to a specific plugin method call, passing only that plugin's options.

AnyError[methodName](...args, options[pluginName])
error[methodName](...args, options[pluginName])

Aggregate errors

The errors option can now be used to aggregate multiple errors into one, similarly to new AggregateError(errors).

Breaking changes

Creating error classes

The main function now returns the base error class AnyError

AnyError.subclass(name) must be used to create each error class. The first one must now be named UnknownError.

Before:

export const {
  // Custom error classes
  InputError,
  AuthError,
  DatabaseError,
  // Error handler
  errorHandler,
} = modernErrors(['InputError', 'AuthError', 'DatabaseError'])

After:

// Base error class
export const AnyError = modernErrors()

export const UnknownError = AnyError.subclass('UnknownError')
export const InputError = AnyError.subclass('InputError')
export const AuthError = AnyError.subclass('AuthError')
export const DatabaseError = AnyError.subclass('DatabaseError')

Error handler

errorHandler() has been renamed to AnyError.normalize()

Before:

const { errorHandler } = modernErrors(errorNames)

const normalizedError = errorHandler(error)

After:

const AnyError = modernErrors()

const normalizedError = AnyError.normalize(error)

Custom classes

Error classes can now be fully customized using the custom option: constructors, methods, etc. This replaces the previous onCreate option.

Before:

modernErrors({
  onCreate: (error, options) => {
    const { filePath } = options

    if (typeof filePath !== 'string') {
      throw new TypeError('filePath must be a string.')
    }

    error.filePath = filePath
  },
})

After:

export const InputError = AnyError.subclass('InputError', {
  custom: class extends AnyError {
    constructor(message, options = {}) {
      super(message, options)

      const { filePath } = options

      if (typeof filePath !== 'string') {
        throw new TypeError('filePath must be a string.')
      }

      this.filePath = filePath
    }
  },
})

Error properties

Error properties must now be set using props.{propName} instead of {propName}.

Before:

throw new InputError('...', { filePath: '/path' })

After:

throw new InputError('...', { props: { filePath: '/path' } })

Bug reports

The bugsUrl option has been renamed to bugs. It cannot be a function anymore. It also requires adding the modern-errors-bugs plugin.

A few bug fixes related to using the bugs option twice have also been fixed.

Before:

throw new InputError('...', {
  bugsUrl: 'https://github.com/my-name/my-project/issues',
})

After:

throw new InputError('...', {
  bugs: 'https://github.com/my-name/my-project/issues',
})

Serialization/parsing

parse() has been renamed to AnyError.parse(). AnyError.parse() and error.toJSON() also require adding the modern-errors-serialize plugin.

Serialization and parsing now recurse deeply over objects and arrays.

Before:

const { parse } = modernErrors(errorNames)

const errorObject = JSON.parse(errorString)
const error = parse(errorObject)

After:

import modernErrorsSerialize from 'modern-errors-serialize'

const AnyError = modernErrors([modernErrorsSerialize])

const errorObject = JSON.parse(errorString)
const error = AnyError.parse(errorObject)

Error wrapping

To wrap an error without changing its class, AnyError must now be used instead of Error. When wrapping an error, its cause and bugs are now merged right away, instead of when AnyError.normalize() is called.

Before:

throw new Error('Could not read the file.', { cause })

After:

throw new AnyError('Could not read the file.', { cause })

Checking error classes

We now recommend using instanceof instead of error.name to check error classes.

Before:

if (error.name === 'InputError') {
  // ...
}

After:

if (error instanceof InputError) {
  // ...
}

AnyError can now be used to check for any errors from a specific library.

if (error instanceof AnyError) {
  // ...
}

TypeScript types

TypeScript support has been greatly improved and is now fully tested. Most types have changed: if you were using them, please check the new documentation here.

Exporting error classes

Error classes should now be exported to be re-used across modules.

License

Switch to MIT license.

3.1.1

Bug fixes

  • Fix the bugsUrl option when the error has a known type

3.1.0

Features

3.0.0

Breaking changes

2.0.2

Bug fixes

  • Fix TypeScript types of parse()

2.0.1

Bug fixes

  • Fix TypeScript types

2.0.0

Breaking changes

  • The error names must now be passed as argument

1.5.0

Features

  • Serialize/parse errors

1.4.1

Bug fixes

  • Fix using the in operator on the return value

1.4.0

Features

  • Reduce npm package size

1.3.0

Documentation

  • Add documentation about CLI errors

1.2.0

Features

  • Improve error normalization