Package Exports
- modern-errors
Readme
Handle errors like it's 2023 🔮
Error handling framework that is pluggable, minimalist yet featureful.
Features
- ⛑️ Create error classes (including with custom logic)
- 🏷️ Set properties on individual errors or on all errors of the same class
- 🎀 Wrap errors' message, class and properties
- 🚨 Normalize invalid errors (not an
Error
instance, missingstack
, etc.) - 🐞 Separate known and unknown errors
- 🤓 Strict TypeScript types
- 📖 Based on standard JavaScript:
throw
,try/catch
,new Error()
,error.cause
,instanceof
,class
,toJSON()
Plugins
modern-errors-cli
: Handle errors in CLI modulesmodern-errors-process
: Handle process errorsmodern-errors-bugs
: Print where to report bugsmodern-errors-serialize
: Serialize/parse errorsmodern-errors-stack
: Clean stack tracesmodern-errors-http
: Create HTTP error responsesmodern-errors-winston
: Log errors with Winston- 🔌 Create your own plugin
Example
Create error classes.
import modernErrors from 'modern-errors'
// 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')
Throw errors.
throw new InputError('Missing file path.')
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 an `Error` instance
throw AnyError.normalize(error)
}
Use plugins.
import modernErrors from 'modern-errors'
import modernErrorsSerialize from 'modern-errors-serialize'
// Use a plugin to serialize errors as JSON
export const AnyError = modernErrors([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 = AnyError.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 is an ES module and must be loaded using
an import
or import()
statement,
not require()
.
API
modernErrors(plugins?, options?)
plugins
: Plugin[]?
options
: object?
Creates and returns AnyError
.
Options:
- any plugin options
props
: error properties
AnyError
Base error class.
AnyError.subclass(name, options?)
name
: string
options
: object?
Return value: class extends AnyError {}
Creates and returns an error subclass. The first one must be named
UnknownError
.
Subclasses can also call ErrorClass.subclass()
themselves.
Options:
- any plugin options
props
: error propertiescustom
: custom class to add any methods,constructor
or properties
AnyError.normalize(anyException)
anyException
: any
Return value: AnyError
Normalizes invalid errors and assigns the UnknownError
class to unknown errors.
new AnyError(message, options?)
message
: string
options
: object?
Return value: AnyError
Options:
- any plugin options
props
: error propertiescause
: inner error being wrapped. Required withAnyError
, optional with its subclasses.errors
: array of errors being aggregated
Usage
⛑️ Error classes
Create error classes
// Base error class
export const AnyError = modernErrors()
// The first error class must be named "UnknownError"
export const UnknownError = AnyError.subclass('UnknownError')
export const InputError = AnyError.subclass('InputError')
export const AuthError = AnyError.subclass('AuthError')
export const DatabaseError = AnyError.subclass('DatabaseError')
Export error classes
Exporting and documenting error classes (including AnyError
and
UnknownError
) allows consumers to check them. This also
enables sharing error classes between modules.
Check error classes
// Known `InputError`
if (error instanceof InputError) {
// ...
}
// Unknown error (from a specific library)
if (error instanceof UnknownError) {
// ...
}
// Any error (from a specific library)
if (error instanceof AnyError) {
// ...
}
🏷️ Throw errors
Simple errors
throw new InputError('Missing file path.')
Error instance properties
const error = new InputError('...', { props: { isUserError: true } })
console.log(error.isUserError) // true
Error class properties
const InputError = AnyError.subclass('InputError', {
props: { isUserError: true },
})
const error = new InputError('...')
console.log(error.isUserError) // true
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: ...
// ]
// }
🎀 Wrap errors
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's, unless the outer error's class
is AnyError
.
try {
throw new AuthError('...')
} catch (cause) {
// Now an InputError
throw new InputError('...', { cause })
}
try {
throw new AuthError('...')
} catch (cause) {
// Still an AuthError
throw new AnyError('...', { cause })
}
Wrap error options
The outer error's options (props
and
plugin options) replace the inner one's. If the outer error's
class is AnyError
, those are merged instead.
try {
throw new AuthError('...', innerOptions)
} catch (cause) {
// Options are now `outerOptions`. `innerOptions` are discarded.
throw new InputError('...', { ...outerOptions, cause })
}
try {
throw new AuthError('...', innerOptions)
} catch (cause) {
// `outerOptions` are merged with `innerOptions`
throw new AnyError('...', { ...outerOptions, cause })
}
🚨 Normalize errors
Wrapped errors
Any error can be directly passed to the cause
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.
AnyError.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 = AnyError.normalize(invalidError)
// This works: 'Missing file path.'
// `normalizedError` is an `Error` instance.
console.log(normalizedError.message.trim())
}
Top-level error handler
Wrapping a module's main functions with
AnyError.normalize()
ensures every error
being thrown is valid, applies
plugins, and has a class that is either
known or UnknownError
.
export const main = function () {
try {
// ...
} catch (error) {
throw AnyError.normalize(error)
}
}
🐞 Unknown errors
Normalizing unknown errors
An error is unknown if its class was not created by
AnyError.subclass()
. This indicates an
unexpected exception, usually a bug.
AnyError.normalize()
assigns the
UnknownError
class to any unknown error.
try {
return regExp.test(value)
} catch (unknownError) {
// Now an `UnknownError` instance
throw AnyError.normalize(unknownError)
}
Handling unknown errors
Unknown errors should be handled in a try {} catch {}
block and
wrapped with a known class
instead. That block should only cover the statement that might throw in order to
prevent catching other unrelated unknown errors.
try {
return regExp.test(value)
} catch (unknownError) {
// Now an `InputError` instance
throw new InputError('Invalid regular expression:', { cause: unknownError })
}
Using plugins with unknown errors
AnyError.normalize()
is required for
unknown errors to use plugins.
try {
return regExp.test(value)
} catch (unknownError) {
unknownError.examplePluginMethod() // This throws
const normalizedError = AnyError.normalize(unknownError)
normalizedError.examplePluginMethod() // This works
}
🔧 Custom logic
Class custom logic
The custom
option can be used to provide an
error class
with additional methods, constructor
or properties.
export const InputError = AnyError.subclass('InputError', {
// The `class` must extend from `AnyError`
custom: class extends AnyError {
// If a `constructor` is defined, its parameters must be (message, options)
// like `AnyError`
constructor(message, options) {
// Modifying `message` or `options` should be done before `super()`
message += message.endsWith('.') ? '' : '.'
// All arguments should be forwarded to `super()`, including any
// custom `options` or additional `constructor` parameters
super(message, options)
// `name` is automatically added, so this is not necessary
// this.name = 'InputError'
}
isUserInput() {
// ...
}
},
})
const error = new InputError('Wrong user name')
console.log(error.message) // 'Wrong user name.'
console.log(error.isUserInput())
Shared custom logic
ErrorClass.subclass()
can be used to share
logic between error classes.
const SharedError = AnyError.subclass('SharedError', {
custom: class extends AnyError {
// ...
},
})
export const InputError = SharedError.subclass('InputError')
export const AuthError = SharedError.subclass('AuthError')
🔌 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
modernErrors()
's first argument.
npm install modern-errors-{pluginName}
import modernErrors from 'modern-errors'
import modernErrorsBugs from 'modern-errors-bugs'
import modernErrorsSerialize from 'modern-errors-serialize'
export const AnyError = modernErrors([modernErrorsBugs, modernErrorsSerialize])
// ...
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):
- Any error: second argument to
modernErrors()
export const AnyError = modernErrors(plugins, options)
- Any error of multiple classes: using
ErrorClass.subclass()
export const SharedError = AnyError.subclass('SharedError', options)
export const InputError = SharedError.subclass('InputError')
export const AuthError = SharedError.subclass('AuthError')
- Any error of a specific class: second argument to
AnyError.subclass()
export const InputError = AnyError.subclass('InputError', options)
- A specific error: second argument to the error's constructor
throw new InputError('...', options)
- A plugin method call: last argument, passing only that plugin's options
AnyError[methodName](...args, options[pluginName])
error[methodName](...args, options[pluginName])
Custom plugins
Please see the following documentation to create your own plugin.
🤓 TypeScript
Please see the following documentation for information about TypeScript types.
Modules
This framework brings together a collection of modules which can also be used individually:
error-custom-class
: Create one error classerror-class-utils
: Utilities to properly create error classeserror-serializer
: Convert errors to/from plain objectsnormalize-exception
: Normalize exceptions/errorsis-error-instance
: Check if a value is anError
instancemerge-error-cause
: Merge an error with itscause
set-error-class
: Properly update an error's classset-error-message
: Properly update an error's messageset-error-props
: Properly update an error's propertieshandle-cli-error
: 💣 Error handler for CLI applications 💥log-process-errors
: Show some ❤ to Node.js process errors
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!