Handle errors in a simple, stable, consistent way.
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.
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
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
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))
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.
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 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 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
const InputError = BaseError.subclass('InputError', {
props: { isUserError: true },
})
const error = new InputError('...')
console.log(error.isUserError) // true
const error = new InputError('...', { props: { isUserError: true } })
console.log(error.isUserError) // true
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`
throw new InputError('Missing file path.')
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 })
}
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 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 })
}
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())
}
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 })
}
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)
}
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 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
, 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())
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
: 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 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
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!
ehmicky 💻 🎨 🤔 📖 |
const_var 🤔 💬 |
Andy Brenneke 🤔 💬 |
Graham Fisher 🐛 |
renzor 💬 🤔 |
Eugene 💻 🐛 |
Jonathan Chambers |