Handle errors like it's 2023 🔮
Error handling framework that is pluggable, minimalist yet featureful.
- ⛑️ 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()
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-clean
: Clean stack tracesmodern-errors-http
: Create HTTP error responsesmodern-errors-winston
: Log errors with Winston- 🔌 Create your own plugin
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')
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 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))
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 >=14.18.0 and
browsers.
It is an ES module and must be loaded using
an import
or import()
statement,
not require()
.
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')
Exporting and documenting all error classes (except for
ModernError
) allows consumers to check them. This also enables
sharing error classes between modules.
if (error instanceof InputError) {
// ...
}
ErrorClass.subclass()
returns a
subclass.
Parent classes' options are merged to 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
throw new InputError('Missing file path.')
const error = new InputError('...', { props: { isUserError: true } })
console.log(error.isUserError) // true
const InputError = BaseError.subclass('InputError', {
props: { isUserError: true },
})
const error = new InputError('...')
console.log(error.isUserError) // true
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: ...
// ]
// }
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 })
}
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 })
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 })
}
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 })
}
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 })
}
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())
}
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 = function () {
try {
// ...
} catch (error) {
throw BaseError.normalize(error, UnknownError)
}
}
An error is unknown if its class is not a subclass of the
BaseError
. This indicates an unexpected exception,
usually a bug.
BaseError.normalize(error, UnknownError)
assigns the UnknownError
class to those errors. This is usually performed in
the top-level error handler.
export const UnknownError = BaseError.subclass('UnknownError')
try {
return regExp.test(value)
} catch (error) {
// Now an `UnknownError` instance
throw BaseError.normalize(error, UnknownError)
}
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 (error) {
// Now an `InputError` instance
throw new InputError('Invalid regular expression:', { cause: error })
}
Plugins extend modern-errors
features. All available plugins are
listed here.
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],
})
// ...
Please see the following documentation to create your own plugin.
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
ModernError.subclass()
export const BaseError = ModernError.subclass('BaseError', options)
- Any error of a specific class (and its subclasses): second argument to
ErrorClass.subclass()
export const InputError = BaseError.subclass('InputError', options)
- A specific error: second argument to
new ErrorClass()
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])
The custom
option can be used to provide an error class
with additional methods, constructor
or properties.
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)
constructor(message, options) {
message += message.endsWith('.') ? '' : '.'
super(message, options)
}
isUserInput() {
// ...
}
},
})
const error = new InputError('Wrong user name')
console.log(error.message) // 'Wrong user name.'
console.log(error.isUserInput())
Please see the following documentation for information about TypeScript types.
Top-level ErrorClass
.
name
: string
options
: ClassOptions?
Creates and returns a child ErrorClass
.
Type: object
Type: Plugin[]
Type: class extends ErrorClass {}
Custom class to add any methods, constructor
or properties.
Any plugin options can also be specified.
message
: string
options
: InstanceOptions?
Return value: Error
Type: object
Type: any
Inner error being wrapped.
Type: any[]
Array of errors being aggregated.
Any plugin options can also be specified.
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.
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
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.
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!