This document explains how to create a plugin for modern-errors. To learn how
to install, use
and configure plugins, please refer to the
main documentation instead.
Plugins can add error:
- Properties:
error.message,error.stackor any othererror.* - Instance methods:
ErrorClass.exampleMethod(error, ...args)orerror.exampleMethod(...args) - Static methods:
ErrorClass.exampleMethod(...args)
export default {
name: 'secret',
properties: ({ error }) => ({
message: error.message.replaceAll('secret', '******'),
}),
}import ModernError from 'modern-errors'
import secretPlugin from './secret.js'
const BaseError = ModernError.subclass('BaseError', { plugins: [secretPlugin] })
const error = new BaseError('Message with a secret')
console.log(error.message) // 'Message with a ******'Existing plugins can be used for inspiration.
The following directory contains a template to start a new plugin, including types and tests.
Plugins are plain objects with a
default export.
All members are optional except for name.
export default {
// Name used to configure the plugin
name: 'example',
// Set error properties
properties: (info) => ({}),
// Add instance methods like `ErrorClass.exampleMethod(error, ...args)` or
// `error.exampleMethod(...args)`
instanceMethods: {
exampleMethod: (info, ...args) => {
// ...
},
},
// Add static methods like `ErrorClass.staticMethod(...args)`
staticMethods: {
staticMethod: (info, ...args) => {
// ...
},
},
// Validate and normalize options
getOptions: (options, full) => options,
// Determine if a value is plugin's options
isOptions: (options) => typeof options === 'boolean',
}Type: string
Plugin's name. It is used to configure the plugin's options.
Only lowercase letters must be used (as opposed to _ - . or uppercase
letters).
// Users configure this plugin using
// `ErrorClass.subclass('ErrorName', { example: ... })`
// or `new ErrorClass('...', { example: ... })
export default {
name: 'example',
}Type: (info) => object
Set properties on error.* (including message or stack). The properties to
set must be returned as an object.
Error properties that are internal or secret can be prefixed with _. This
makes them
non-enumerable,
which prevents iterating or logging them.
export default {
name: 'example',
// Sets `error.example: true`
properties: () => ({ example: true }),
}Type: (info, ...args) => any
Add error instance methods like ErrorClass.methodName(error, ...args) or
error.methodName(...args). Unlike static methods,
this should be used when the method's main argument is an error instance.
The first argument info is provided by modern-errors. The error
argument is passed as info.error. The other ...args are forwarded
from the method's call.
export default {
name: 'example',
// `ErrorClass.concatMessage(error, "one")` or `error.concatMessage("one")`
// return `${error.message} - one`
instanceMethods: {
concatMessage: (info, string) => `${info.error.message} - ${string}`,
},
}Invalid errors passed as error argument are
automatically normalized. This only occurs
when using ErrorClass.methodName(error, ...args), not
error.methodName(...args). For this reason, we discourage using or documenting
error.methodName(...args) unless there is a use case for it, since users might
accidentally call it without normalizing
error first.
try {
return regExp.test(value)
} catch (error) {
ErrorClass.exampleMethod(error, ...args) // This works
ErrorClass.normalize(error).exampleMethod(...args) // This works
error.exampleMethod(...args) // This throws
}Type: (info, ...args) => any
Add error static methods like ErrorClass.methodName(...args). Unlike
instance methods, this should be used when the
method's main argument is not an error instance.
The first argument info is provided by modern-errors.
info.error is not defined. The other ...args are forwarded from
the method's call.
export default {
name: 'example',
// `ErrorClass.multiply(2, 3)` returns `6`
staticMethods: {
multiply: (info, first, second) => first * second,
},
}Type: (options, full) => options
Normalize and return the plugin's options.
Required to use plugin options.
If options are invalid, an Error should be thrown. The error message is
automatically prepended with Invalid "${plugin.name}" options:. Regular
Errors
should be thrown, as opposed to using modern-errors itself.
The plugin's options's type can be anything.
export default {
name: 'example',
getOptions: (options = true) => {
if (typeof options !== 'boolean') {
throw new Error('It must be true or false.')
}
return options
},
}Plugin users can pass additional options
at multiple stages. Each stage calls
getOptions().
- When error classes are defined:
ErrorClass.subclass('ExampleError', options) - When new errors are created:
new ErrorClass('message', options) - As a last argument to instance methods or static methods
full is a boolean parameter indicating whether the options might still be
partial. It is false in the first stage above, true in the others.
When full is false, any logic validating required properties should be
skipped. The same applies to properties depending on each other.
export default {
name: 'example',
getOptions: (options, full) => {
if (typeof options !== 'object' || options === null) {
throw new Error('It must be a plain object.')
}
if (full && options.apiKey === undefined) {
throw new Error('"apiKey" is required.')
}
return options
},
}Type: (options) => boolean
Plugin users can pass the plugin's options as
the last argument of any plugin method (instance
or static). isOptions() determines whether the
last argument of a plugin method are options or not. This should be defined if
the plugin has any method with arguments.
If options are invalid but can be determined not to be the last argument of
any plugin's method, isOptions() should still return true. This allows
getOptions() to validate them and throw proper error messages.
// `ErrorClass.exampleMethod(error, 'one', true)` results in:
// options: true
// args: ['one']
// `ErrorClass.exampleMethod(error, 'one', 'two')` results in:
// options: undefined
// args: ['one', 'two']
export default {
name: 'example',
isOptions: (options) => typeof options === 'boolean',
getOptions: (options) => options,
instanceMethod: {
exampleMethod: ({ options }, ...args) => {
// ...
},
},
}info is a plain object passed as the first argument to
properties(), instance methods
and static methods.
Its members are readonly and should not be directly mutated. Exception:
instance methods can mutate
info.error.
Type: Error
Normalized error instance. This is not defined in static methods.
export default {
name: 'example',
properties: ({ error }) => ({ isInputError: error.name === 'InputError' }),
}Type: ErrorClass
Current error class.
export default {
name: 'example',
instanceMethods: {
addErrors: ({ error, ErrorClass }, errors = []) => {
error.errors = errors.map(ErrorClass.normalize)
},
},
}Type: ErrorClass[]
Array containing both the current error class and all its subclasses (including deep ones).
export default {
name: 'example',
staticMethods: {
isKnownErrorClass: ({ ErrorClasses }, value) =>
ErrorClasses.includes(value),
},
}Type: any
Plugin's options, as returned by getOptions().
export default {
name: 'example',
getOptions: (options) => options,
// `new ErrorClass('message', { example: value })` sets `error.example: value`
properties: ({ options }) => ({ example: options }),
}Type: (Error) => info
Returns the info object from a specific Error. All members are
present except for info.errorInfo itself.
export default {
name: 'example',
staticMethods: {
getLogErrors:
({ errorInfo }) =>
(errors) => {
errors.forEach((error) => {
const { options } = errorInfo(error)
console.error(options.example?.stack ? error.stack : error.message)
})
},
},
}Any plugin's types are automatically exposed to its TypeScript users.
The types of getOptions()'s parameters are used to validate the
plugin's options.
// Any `{ example }` plugin option passed by users will be validated as boolean
export default {
name: 'example' as const,
getOptions: (options: boolean): object => {
// ...
},
}The name property should be typed as const so it can be used to
validate the plugin's options.
export default {
name: 'example' as const,
// ...
}The types of properties(),
instanceMethods and
staticMethods are also exposed to plugin users.
Please note
generics are
currently ignored.
// Any `ErrorClass.exampleMethod(error, input)` or `error.exampleMethod(input)`
// call will be validated
export default {
// ...
instanceMethods: {
exampleMethod: (info: Info['instanceMethods'], input: boolean): void => {},
},
}The info parameter can be typed with Info['properties'],
Info['instanceMethods'], Info['staticMethods'] or Info['errorInfo'].
import type { Info } from 'modern-errors'
export default {
// ...
properties: (info: Info['properties']) => {
// ...
},
}info.options type can be passed as a generic type.
export default {
// ...
properties: (info: Info<boolean>['properties']) => {
// `info.options` is `boolean`
if (info.options) {
// ...
}
},
}A Plugin type is available to validate the plugin's shape.
satisfies Plugin
should be used (as opposed to const plugin: Plugin = { ... }) to prevent
widening it and removing any specific types declared by that plugin.
import type { Plugin } from 'modern-errors'
export default {
// ...
} satisfies PluginIf the plugin is published on npm, we recommend the following conventions:
- The npm package name should be
[@scope/]modern-errors-${plugin.name} - The repository name should match the npm package name
-
"modern-errors"and"modern-errors-plugin"should be added as bothpackage.jsonkeywordsand GitHub topics -
"modern-errors"should be added in thepackage.json'speerDependencies, not in the productiondependencies,devDependenciesnorbundledDependencies. Its semver range should start with^. Also,peerDependenciesMeta.modern-errors.optionalshould not be used. - The
READMEshould document how to: - The plugin should export its types for TypeScript users
- Please create an issue on the
modern-errorsrepository so we can add the plugin to the list of available ones! 🎉
Options types should ideally be JSON-serializable. This allows preserving them when errors are serialized/parsed. In particular, functions and class instances should be avoided in plugin options, when possible.
modern-errors provides with a pattern for options that enables them to be:
- Passed at multiple stages
- Validated as soon as users pass them
- Automatically typed
- Consistent between different plugins
export default {
name: 'example',
getOptions: (options) => options,
instanceMethods: {
exampleMethod: (info) => {
console.log(info.options.exampleOption)
},
},
}Plugins should avoid alternatives since they would lose those benefits. This includes:
- Error method arguments, for configuration options
export default {
name: 'example',
instanceMethods: {
exampleMethod: (exampleOption) => {
console.log(exampleOption)
},
},
}- Error properties
export default {
name: 'example',
instanceMethods: {
exampleMethod: (info) => {
console.log(info.error.exampleOption)
},
},
}- Top-level objects
export const pluginOptions = {}
export default {
name: 'example',
instanceMethods: {
exampleMethod: () => {
console.log(pluginOptions.exampleOption)
},
},
}- Functions taking options as input and returning the plugin
export default (exampleOption) => ({
name: 'example',
instanceMethods: {
exampleMethod: () => {
console.log(exampleOption)
},
},
})Plugins should be usable by libraries. Therefore, modifying global objects (such
as Error.prepareStackTrace()) should be avoided.
Other state objects, such as class instances or network connections, should not
be kept in the global state. This ensures plugins are concurrency-safe, i.e. can
be safely used in parallel async logic. Instead, plugins should either:
- Provide with methods returning such objects
- Let users create those objects and pass them as arguments to plugin methods
If the plugin contains some logic that is not specific to modern-errors,
splitting it to a separate library allows using it without modern-errors. This
also keeps the plugin smaller and focused on integrating with modern-errors.
Some examples include:
modern-errors-cli(underlying module:handle-cli-error)modern-errors-beautiful(underlying module:beautiful-error)modern-errors-serialize(underlying module:error-serializer)modern-errors-process(underlying module:log-process-errors)modern-errors-winston(underlying module:winston-error-format)modern-errors-http(underlying module:error-http-response)modern-errors-switch(underlying module:switch-functional)