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.stack
or any othererror.*
- Error instance methods:
error.exampleMethod()
AnyError
static methods:AnyError.exampleMethod()
The following directory contains examples of a plugin.
Existing plugins can be used for inspiration.
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) {
return {}
},
// Add error instance methods like `error.exampleMethod(...args)`
instanceMethods: {
exampleMethod(info, ...args) {
// ...
},
},
// Add `AnyError` static methods like `AnyError.staticMethod(...args)`
staticMethods: {
staticMethod(info, ...args) {
// ...
},
},
// Validate and normalize options
getOptions(options, full) {
return options
},
// Determine if a value is plugin's options
isOptions(options) {
return 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 `modernErrors([plugin], { 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.
export default {
name: 'example',
// Sets `error.example: true`
properties() {
return { example: true }
},
}
Type: (info, ...args) => any
Add error instance methods like error.methodName(...args)
.
The first argument info
is provided by modern-errors
. The other
...args
are forwarded from the method's call.
If the logic involves an error
instance or error-specific options
, instance
methods should be preferred over static methods.
Otherwise, static methods should be used.
export default {
name: 'example',
// `error.concatMessage("one")` returns `${error.message} - one`
instanceMethods: {
concatMessage({ error }, string) {
return `${error.message} - ${string}`
},
},
}
Type: (info, ...args) => any
Add AnyError
static methods like
AnyError.methodName(...args)
.
The first argument info
is provided by modern-errors
. The other
...args
are forwarded from the method's call.
export default {
name: 'example',
// `AnyError.multiply(2, 3)` returns `6`
staticMethods: {
multiply(info, first, second) {
return 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
Error
s
should be thrown, as opposed to using modern-errors
itself.
The plugin's options
can have any type.
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:
modernErrors(plugins, options)
andAnyError.subclass('ErrorClass', 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.
// `error.exampleMethod('one', true)` results in:
// options: true
// args: ['one']
// `error.exampleMethod('one', 'two')` results in:
// options: undefined
// args: ['one', 'two']
export default {
name: 'example',
isOptions(options) {
return typeof options === 'boolean'
},
getOptions(options) {
return options
},
instanceMethod: {
exampleMethod({ options }, ...args) {
// ...
},
},
}
info
is a plain object passed as the first argument to
properties()
, instance methods
and static methods.
info.error
and info.showStack
are not passed to
static methods.
Its members are readonly and should not be mutated, except for
info.error
inside instance methods
(not inside properties()
).
Type: Error
Normalized error instance.
export default {
name: 'example',
properties({ error }) {
return { isInputError: error.name === 'InputError' }
},
}
Type: any
Plugin's options, as returned by getOptions()
.
export default {
name: 'example',
getOptions(options) {
return options
},
// `new ErrorClass('message', { example: value })` sets `error.example: value`
properties({ options }) {
return { example: options }
},
}
Type: boolean
Hints whether error.stack
should be printed or not.
This is true
if the error (or one of its inner
errors) is unknown, and false
otherwise.
If a plugin prints error.stack
optionally, showStack
can be used as the
default value of a stack
boolean option. This allows users to decide whether
to print error.stack
or not, while still providing with a good default
behavior.
export default {
name: 'example',
getOptions(options) {
// ...
},
instanceMethods: {
log({ error, showStack, options: { stack = showStack } }) {
console.log(stack ? error.stack : error.message)
},
},
}
Type: typeof AnyError
Reference to AnyError
. This can be used to
wrap errors or to call
AnyError.normalize()
or
error instanceof AnyError
.
export default {
name: 'example',
instanceMethods: {
addErrors({ error, AnyError }, errors = []) {
error.errors = errors.map(AnyError.normalize)
},
},
}
Type: object
Object with all error classes created with
AnyError.subclass()
or
ErrorClass.subclass()
.
export default {
name: 'example',
staticMethods: {
isKnownErrorClass({ ErrorClasses }, value) {
return Object.values(ErrorClasses).includes(value)
},
},
}
Type: (Error) => info
Returns the info
object from a specific Error
, except from
info.AnyError
, info.ErrorClasses
and
info.errorInfo
.
export default {
name: 'example',
staticMethods: {
getLogErrors({ errorInfo }) {
return function logErrors(errors) {
errors.forEach((error) => {
const { showStack } = errorInfo(error)
console.log(showStack ? 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 `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']) {
// ...
},
}
A Plugin
type is available to validate the plugin's shape.
satisfies Plugin
should be used (not 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 Plugin
If 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.json
keywords
and GitHub topics -
"modern-errors"
should be added in thepackage.json
'speerDependencies
, not in the productiondependencies
,devDependencies
norbundledDependencies
. Its semver range should start with^
. Also,peerDependenciesMeta.modern-errors.optional
should not be used. - The
README
should document how to: - The plugin should export its types for TypeScript users
- Please create an issue on the
modern-errors
repository 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
consistent pattern for options. Plugins should
avoid alternatives like:
- Functions taking options as input and returning the plugin:
(options) => plugin
- Setting options using the properties/methods of the plugin or another object
Plugins should be usable by libraries. Therefore, modifying global objects (such
as Error.prepareStackTrace()
) should be avoided.
WeakMap
s should be used to keep error-specific internal state, as opposed to
using error properties (even with symbol
keys).
const state = new WeakMap()
export default {
name: 'example',
instanceMethods: {
exampleMethod({ error }) {
state.set(error, { example: true })
},
},
}
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