modern-errors
Version:
Handle errors in a simple, stable, consistent way
730 lines (545 loc) • 23.8 kB
Markdown
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ehmicky/design/main/modern-errors/modern-errors_dark.svg"/>
<img alt="modern-errors logo" src="https://raw.githubusercontent.com/ehmicky/design/main/modern-errors/modern-errors.svg" width="600"/>
</picture>
[](https://www.npmjs.com/package/modern-errors)
[](https://unpkg.com/modern-errors?module)
[](/src/main.d.ts)
[](https://codecov.io/gh/ehmicky/modern-errors)
[](https://bundlephobia.com/package/modern-errors)
[](https://fosstodon.org/@ehmicky)
[](https://medium.com/@ehmicky)
Handle errors in a simple, stable, consistent way.
# Features
Simple patterns to:
- ⛑️ Create error [classes](#create-error-classes)
- 🏷️ Set error [properties](#%EF%B8%8F-error-properties)
- 🎀 [Wrap](#-wrap-errors) or [aggregate](#aggregate-errors) errors
- 🐞 Separate known and [unknown](#-unknown-errors) errors
Stability:
- 🚨 [Normalize](#-normalize-errors) invalid errors
- 🛡️ 100% [test coverage](https://app.codecov.io/gh/ehmicky/modern-errors)
- 🤓 Strict [TypeScript types](docs/typescript.md)
# Plugins
- [`modern-errors-cli`](https://github.com/ehmicky/modern-errors-cli): Handle
errors in CLI modules
- [`modern-errors-beautiful`](https://github.com/ehmicky/modern-errors-beautiful):
Prettify error messages and stacks
- [`modern-errors-process`](https://github.com/ehmicky/modern-errors-process):
Handle process errors
- [`modern-errors-bugs`](https://github.com/ehmicky/modern-errors-bugs): Print
where to report bugs
- [`modern-errors-serialize`](https://github.com/ehmicky/modern-errors-serialize):
Serialize/parse errors
- [`modern-errors-clean`](https://github.com/ehmicky/modern-errors-clean): Clean
stack traces
- [`modern-errors-http`](https://github.com/ehmicky/modern-errors-http): Create
HTTP error responses
- [`modern-errors-winston`](https://github.com/ehmicky/modern-errors-winston):
Log errors with Winston
- [`modern-errors-switch`](https://github.com/ehmicky/modern-errors-switch):
Execute class-specific logic
- 🔌 Create your [own plugin](docs/plugins.md)
# Example
Create error [classes](#%EF%B8%8F-error-classes).
```js
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](#%EF%B8%8F-error-properties).
```js
throw new InputError('Invalid file path', { props: { filePath: '/...' } })
```
[Wrap](#-wrap-errors) errors.
```js
try {
// ...
} catch (cause) {
throw new InputError('Could not read the file.', { cause })
}
```
[Normalize](#-normalize-errors) errors.
<!-- eslint-disable no-throw-literal -->
```js
try {
throw 'Missing file path.'
} catch (error) {
// Normalized from a string to a `BaseError` instance
throw BaseError.normalize(error)
}
```
Use [plugins](#-plugins).
```js
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
```bash
npm install modern-errors
```
If any [plugin](#-plugins) is used, it must also be installed.
```bash
npm install modern-errors-{pluginName}
```
This package works in both Node.js >=18.18.0 and
[browsers](https://raw.githubusercontent.com/ehmicky/dev-tasks/main/src/browserslist).
This is an ES module. It must be loaded using
[an `import` or `import()` statement](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c),
not `require()`. If TypeScript is used, it must be configured to
[output ES modules](https://www.typescriptlang.org/docs/handbook/esm-node.html),
not CommonJS.
# Usage
## ⛑️ Error classes
### Create error classes
```js
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
```js
if (error instanceof InputError) {
// ...
}
```
### Error subclasses
[`ErrorClass.subclass()`](#errorclasssubclassname-options) returns a
[subclass](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/extends).
Parent classes' [options](#options) are merged with their subclasses.
```js
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
```js
const InputError = BaseError.subclass('InputError', {
props: { isUserError: true },
})
const error = new InputError('...')
console.log(error.isUserError) // true
```
### Error instance properties
```js
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties),
which prevents iterating or logging them.
<!-- eslint-disable no-underscore-dangle -->
```js
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
```js
throw new InputError('Missing file path.')
```
### Wrap inner error
Any error's [message](#wrap-error-message), [class](#wrap-error-class) and
[options](#wrap-error-options) can be wrapped using the
[standard](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause)
[`cause` option](#optionscause).
Instead of being set as a `cause` property, the inner error is directly
[merged](https://github.com/ehmicky/merge-error-cause) to the outer error,
including its
[`message`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/message),
[`stack`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/stack),
[`name`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/name),
[`AggregateError.errors`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError)
and any [additional property](#%EF%B8%8F-error-properties).
```js
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.
```js
const cause = new InputError('File does not exist.')
// InputError: File does not exist.
throw new InputError('', { cause })
```
```js
// InputError: File does not exist.
// Could not read the file.
throw new InputError('Could not read the file.', { cause })
```
```js
// InputError: Could not read the file: File does not exist.
throw new InputError(`Could not read the file:`, { cause })
```
```js
// 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.
```js
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`](#create-error-classes).
```js
try {
throw new AuthError('...')
} catch (cause) {
// Still an AuthError
throw new BaseError('...', { cause })
}
```
### Wrap error options
The outer error's [`props`](#%EF%B8%8F-error-properties) and
[plugin options](#plugin-options) are merged.
```js
try {
throw new AuthError('...', innerOptions)
} catch (cause) {
// `outerOptions` are merged with `innerOptions`
throw new BaseError('...', { ...outerOptions, cause })
}
```
### Aggregate errors
The [`errors` option](#optionserrors) aggregates multiple errors into one. This
is like
[`new AggregateError(errors)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError/AggregateError)
except that it works with any error class.
```js
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`](#wrap-inner-error) or
[`errors`](#aggregate-errors) option, even if it is [invalid](#invalid-errors),
[unknown](#-unknown-errors) or not
[normalized](#errorclassnormalizeerror-newerrorclass).
```js
try {
// ...
} catch (cause) {
throw new InputError('...', { cause })
}
```
### Invalid errors
Manipulating errors that are not
[`Error` instances](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)
or that have
[invalid properties](https://github.com/ehmicky/normalize-exception#features)
can lead to unexpected bugs.
[`BaseError.normalize()`](#errorclassnormalizeerror-newerrorclass) fixes that.
<!-- eslint-disable no-throw-literal -->
```js
try {
throw 'Missing file path.'
} catch (invalidError) {
// This fails: `invalidError.message` is `undefined`
console.log(invalidError.message.trim())
}
```
<!-- eslint-disable no-throw-literal -->
```js
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](#wrap-error-class) with a [specific class](#create-error-classes).
That block should only cover the statement that might throw in order to prevent
catching other unrelated errors.
<!-- eslint-skip -->
```js
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](#handling-known-errors), it is
considered _unknown_. This indicates an unexpected exception, usually a bug.
[`BaseError.normalize(error, UnknownError)`](#errorclassnormalizeerror-newerrorclass)
assigns the `UnknownError` class to those errors.
```js
export const UnknownError = BaseError.subclass('UnknownError')
```
<!-- eslint-skip -->
```js
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)`](#errorclassnormalizeerror-newerrorclass)
ensures every error being thrown is [valid](#invalid-errors), applies
[plugins](#-plugins), and has a class that is either
[_known_](#create-error-classes) or [`UnknownError`](#-unknown-errors).
```js
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](#plugins).
### Adding plugins
To use a plugin, please install it, then pass it to the
[`plugins` option](#optionsplugins).
```bash
npm install modern-errors-{pluginName}
```
<!-- eslint-disable import/order -->
```js
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](docs/plugins.md) to create your own
plugin.
### Plugin options
Most plugins can be configured with options. The option's name is the same as
the plugin.
```js
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):
- Any error: second argument to [`ModernError.subclass()`](#options-1)
```js
export const BaseError = ModernError.subclass('BaseError', options)
```
- Any error of a specific class (and its subclasses): second argument to
[`ErrorClass.subclass()`](#options-1)
```js
export const InputError = BaseError.subclass('InputError', options)
```
- A specific error: second argument to [`new ErrorClass()`](#options-3)
```js
throw new InputError('...', options)
```
- A plugin method call: last argument, passing only that plugin's options
```js
ErrorClass[methodName](...args, options[pluginName])
```
```js
error[methodName](...args, options[pluginName])
```
## 🔧 Custom logic
The [`custom` option](#optionscustom) can be used to provide an error `class`
with additional methods, `constructor`, properties or options.
<!-- eslint-disable no-param-reassign, fp/no-mutation,
class-methods-use-this -->
```js
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](docs/typescript.md) for information
about TypeScript types.
# API
## ModernError
Top-level `ErrorClass`.
## ErrorClass.subclass(name, options?)
`name`: `string`\
`options`: [`ClassOptions?`](#options)
Creates and returns a child `ErrorClass`.
### options
#### options.props
_Type_: `object`
[Error class properties](#error-class-properties).
#### options.plugins
_Type_: [`Plugin[]`](#-plugins)
#### options.custom
_Type_: `class extends ErrorClass {}`
[Custom class](#-custom-logic) to add any methods, `constructor` or properties.
#### options.\*
Any [plugin options](#plugin-options) can also be specified.
## new ErrorClass(message, options?)
`message`: `string`\
`options`: [`InstanceOptions?`](#options-2)\
_Return value_: `Error`
### options
#### options.props
_Type_: `object`
[Error instance properties](#error-instance-properties).
#### options.cause
_Type_: [`any`](#wrapped-errors)
Inner error being [wrapped](#-wrap-errors).
#### options.errors
_Type_: `any[]`
Array of errors being [aggregated](#aggregate-errors).
#### options.\*
Any [plugin options](#plugin-options) can also be specified.
## ErrorClass.normalize(error, NewErrorClass?)
`error`: `Error | any`\
`NewErrorClass`: subclass of `ErrorClass`\
_Return value_: `Error`
Normalizes [invalid errors](#invalid-errors).
If `error` is an instance of `ErrorClass` (or one of its subclasses), it is left
as is. Otherwise, it is
[converted to `NewErrorClass`](#normalizing-unknown-errors), which defaults to
`ErrorClass` itself.
# Modules
This framework brings together a collection of modules which can also be used
individually:
- [`error-custom-class`](https://github.com/ehmicky/error-custom-class): Create
one error class
- [`error-class-utils`](https://github.com/ehmicky/error-class-utils): Utilities
to properly create error classes
- [`error-serializer`](https://github.com/ehmicky/error-serializer): Convert
errors to/from plain objects
- [`normalize-exception`](https://github.com/ehmicky/normalize-exception):
Normalize exceptions/errors
- [`is-error-instance`](https://github.com/ehmicky/is-error-instance): Check if
a value is an `Error` instance
- [`merge-error-cause`](https://github.com/ehmicky/merge-error-cause): Merge an
error with its `cause`
- [`set-error-class`](https://github.com/ehmicky/set-error-class): Properly
update an error's class
- [`set-error-message`](https://github.com/ehmicky/set-error-message): Properly
update an error's message
- [`wrap-error-message`](https://github.com/ehmicky/wrap-error-message):
Properly wrap an error's message
- [`set-error-props`](https://github.com/ehmicky/set-error-props): Properly
update an error's properties
- [`set-error-stack`](https://github.com/ehmicky/set-error-stack): Properly
update an error's stack
- [`handle-cli-error`](https://github.com/ehmicky/handle-cli-error): 💣 Error
handler for CLI applications 💥
- [`beautiful-error`](https://github.com/ehmicky/beautiful-error): Prettify
error messages and stacks
- [`log-process-errors`](https://github.com/ehmicky/log-process-errors): Show
some ❤ to Node.js process errors
- [`error-http-response`](https://github.com/ehmicky/error-http-response):
Create HTTP error responses
- [`winston-error-format`](https://github.com/ehmicky/winston-error-format): Log
errors with Winston
# Support
For any question, _don't hesitate_ to [submit an issue on GitHub](../../issues).
Everyone is welcome regardless of personal background. We enforce a
[Code of conduct](CODE_OF_CONDUCT.md) 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](CONTRIBUTING.md). Pull requests are welcome!
<!-- Thanks go to our wonderful contributors: -->
<!-- ALL-CONTRIBUTORS-LIST:START -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://fosstodon.org/@ehmicky"><img src="https://avatars2.githubusercontent.com/u/8136211?v=4?s=100" width="100px;" alt="ehmicky"/><br /><sub><b>ehmicky</b></sub></a><br /><a href="https://github.com/ehmicky/modern-errors/commits?author=ehmicky" title="Code">💻</a> <a href="#design-ehmicky" title="Design">🎨</a> <a href="#ideas-ehmicky" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/ehmicky/modern-errors/commits?author=ehmicky" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bhvngt"><img src="https://avatars.githubusercontent.com/u/79074469?v=4?s=100" width="100px;" alt="const_var"/><br /><sub><b>const_var</b></sub></a><br /><a href="#ideas-bhvngt" title="Ideas, Planning, & Feedback">🤔</a> <a href="#question-bhvngt" title="Answering Questions">💬</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/abrenneke"><img src="https://avatars.githubusercontent.com/u/342540?v=4?s=100" width="100px;" alt="Andy Brenneke"/><br /><sub><b>Andy Brenneke</b></sub></a><br /><a href="#ideas-abrenneke" title="Ideas, Planning, & Feedback">🤔</a> <a href="#question-abrenneke" title="Answering Questions">💬</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tgfisher4"><img src="https://avatars.githubusercontent.com/u/49082176?v=4?s=100" width="100px;" alt="Graham Fisher"/><br /><sub><b>Graham Fisher</b></sub></a><br /><a href="https://github.com/ehmicky/modern-errors/issues?q=author%3Atgfisher4" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/renzor-fist"><img src="https://avatars.githubusercontent.com/u/117486829?v=4?s=100" width="100px;" alt="renzor"/><br /><sub><b>renzor</b></sub></a><br /><a href="#question-renzor-fist" title="Answering Questions">💬</a> <a href="#ideas-renzor-fist" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/eugene1g"><img src="https://avatars.githubusercontent.com/u/147496?v=4?s=100" width="100px;" alt="Eugene"/><br /><sub><b>Eugene</b></sub></a><br /><a href="https://github.com/ehmicky/modern-errors/commits?author=eugene1g" title="Code">💻</a> <a href="https://github.com/ehmicky/modern-errors/issues?q=author%3Aeugene1g" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://uk.linkedin.com/in/jonathanmarkchambers/"><img src="https://avatars.githubusercontent.com/u/49592?v=4?s=100" width="100px;" alt="Jonathan Chambers"/><br /><sub><b>Jonathan Chambers</b></sub></a><br /><a href="https://github.com/ehmicky/modern-errors/commits?author=jmchambers" title="Tests">⚠️</a> <a href="https://github.com/ehmicky/modern-errors/issues?q=author%3Ajmchambers" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/heyhey123-git"><img src="https://avatars.githubusercontent.com/u/156066831?v=4?s=100" width="100px;" alt="heyhey123"/><br /><sub><b>heyhey123</b></sub></a><br /><a href="https://github.com/ehmicky/modern-errors/issues?q=author%3Aheyhey123-git" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://vegardit.com"><img src="https://avatars.githubusercontent.com/u/7782055?v=4?s=100" width="100px;" alt="Benjamin Kroeger"/><br /><sub><b>Benjamin Kroeger</b></sub></a><br /><a href="https://github.com/ehmicky/modern-errors/issues?q=author%3Abenkroeger" title="Bug reports">🐛</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->