Handle errors in a simple, stable, consistent way.
Hire me
Please reach out if you're looking for a Node.js API or CLI engineer (11 years of experience). Most recently I have been Netlify Build's and Netlify Plugins' technical lead for 2.5 years. I am available for full-time remote positions.
Features
Simple patterns to:
- ⛑️ Create error classes
- 🏷️ Set error properties
- 🎀 Wrap or aggregate errors
- 🐞 Separate known and unknown errors
Stability:
- 🚨 Normalize invalid errors
- 🛡️ 100% test coverage
- 🤓 Strict TypeScript types
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-clean
: Clean stack tracesmodern-errors-http
: Create HTTP error responsesmodern-errors-winston
: Log errors with Winstonmodern-errors-switch
: Execute class-specific logic- 🔌 Create your own plugin
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):
- 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])
🔧 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
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
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:
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 messagewrap-error-message
: Properly wrap an error's messageset-error-props
: Properly update an error's propertiesset-error-stack
: Properly update an error's stackhandle-cli-error
: 💣 Error handler for CLI applications 💥log-process-errors
: Show some ❤ to Node.js process errorserror-http-response
: Create HTTP error responseswinston-error-format
: Log errors with Winston
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 💻 🎨 🤔 📖 |
const_var 🤔 💬 |
Andy Brenneke 🤔 💬 |
Graham Fisher 🐛 |
renzor 💬 🤔 |
Eugene 💻 🐛 |
Jonathan Chambers |