Handle errors like it's 2022 🔮
Error handling framework that is minimalist yet featureful.
- Create custom error types
- Handle errors from both programmatic and CLI modules
- Wrap inner errors' message, type, or properties
- Automatically separate known and unknown errors
- Unknown errors indicate where to report bugs
- Serialize/parse errors
- Set properties on individual errors, or on all errors of the same type
- Handle invalid errors (not an
Error
instance, missing stack, etc.)
Create custom error types.
// `error.js`
import modernErrors from 'modern-errors'
export const { InputError, AuthError, DatabaseError, errorHandler, parse } =
modernErrors(['InputError', 'AuthError', 'DatabaseError'])
Wrap the main function with the error handler.
import { errorHandler } from './error.js'
export const main = async function (filePath) {
try {
return await readContents(filePath)
} catch (error) {
throw errorHandler(error)
}
}
Throw/re-throw errors.
import { InputError } from './error.js'
const readContents = async function (filePath) {
try {
return await readFile(filePath)
} catch (cause) {
throw new InputError(`Could not read ${filePath}`, { cause })
}
}
npm install modern-errors
This package is an ES module and must be loaded using
an import
or import()
statement,
not require()
.
errorNames
string[]
options
object
Return value: object
Creates custom error types.
Type: CustomErrorType
Any error name passed as argument is returned as an error type.
Type: (anyException) => CustomError
Error handler that should wrap each main function.
Type: (errorObject) => Error
Convert an error plain object into
an Error
instance.
Type: string | URL | ((error: Error) => string | URL | void)
URL where users should report unknown errors.
Type: (error, parameters) => void
Called on any
new CustomErrorType('message', parameters)
. Can be
used to customize error parameters or set
error type properties. By default, any parameters
are set as error properties.
// error.js
import modernErrors from 'modern-errors'
export const { InputError, AuthError, DatabaseError, errorHandler, parse } =
modernErrors(['InputError', 'AuthError', 'DatabaseError'])
Each main function should be wrapped with the errorHandler()
.
import { errorHandler } from './error.js'
export const main = async function (filePath) {
try {
return await readContents(filePath)
} catch (error) {
// `errorHandler()` returns `error`, so `throw` must be used
throw errorHandler(error)
}
}
import { InputError } from './error.js'
const validateFilePath = function (filePath) {
if (filePath === '') {
throw new InputError('Missing file path.')
}
}
Invalid errors are normalized
by errorHandler()
. This includes errors that are not an
Error
instance
or that have
wrong/missing properties.
import { errorHandler } from './error.js'
export const main = function (filePath) {
try {
throw 'Missing file path.'
} catch (error) {
throw errorHandler(error) // Normalized to an `Error` instance
}
}
Errors are re-thrown using the
standard cause
parameter.
This allows wrapping the error message,
properties, or type.
import { InputError } from './error.js'
const readContents = async function (filePath) {
try {
return await readFile(filePath)
} catch (cause) {
throw new InputError(`Could not read ${filePath}`, { cause })
}
}
The errorHandler()
merges all error cause
into a
single error, including their
message
,
stack
,
name
,
AggregateError.errors
and any additional property. This ensures:
error.cause
does not need to be traversed- The stack trace is neither verbose nor redundant, while still keeping all information
The outer error message is appended.
try {
await readFile(filePath)
} catch (cause) {
throw new InputError(`Could not read ${filePath}`, { cause })
// InputError: File does not exist.
// Could not read /example/path
}
If the outer error message ends with :
, it is prepended instead.
throw new InputError(`Could not read ${filePath}:`, { cause })
// InputError: Could not read /example/path: File does not exist.
:
can optionally be followed a newline.
throw new InputError(`Could not read ${filePath}:\n`, { cause })
// InputError: Could not read /example/path:
// File does not exist.
Once errorHandler()
has been applied, the error type can be
checked by its name
. Libraries should document their possible error names, but
do not need to export
their error types.
if (error.name === 'InputError') {
// ...
} else if (error.name === 'UnknownError') {
// ...
}
When re-throwing errors, the outer error type overrides the inner one.
try {
throw new AuthError('Could not authenticate.')
} catch (cause) {
throw new InputError('Could not read the file.', { cause })
// Now an InputError
}
However, the inner error type is kept if the outer one is Error
or
AggregateError
.
try {
throw new AuthError('Could not authenticate.')
} catch (cause) {
throw new Error('Could not read the file.', { cause })
// Still an AuthError
}
All errors should use known types: the ones returned by
modernErrors()
. Errors with an unknown type
should be handled in try {} catch {}
and re-thrown with a
known type instead.
The errorHandler()
assigns the UnknownError
type to any
error with an unknown type.
const getUserId = function (user) {
return user.id
}
getUserId(null) // UnknownError: Cannot read properties of null (reading 'id')
If the bugsUrl
option is a string or URL, any
unknown error will include the following message.
modernErrors({ bugsUrl: 'https://github.com/my-name/my-project/issues' })
Please report this bug at: https://github.com/my-name/my-project/issues
If the bugsUrl
option is a function returning a string or URL, any error
(known or unknown) will include it, unless the return
value is undefined
.
createErrorTypes({
bugsUrl: (error) =>
error.name === 'UnknownError' || error.name === 'PluginError'
? 'https://github.com/my-name/my-project/issues'
: undefined,
})
Unless the onCreate()
option is defined, any parameter is set as
an error property.
const error = new InputError('Could not read the file.', { filePath: '/path' })
console.log(error.filePath) // '/path'
Pass an empty message
in order to set error properties without wrapping the
message
.
try {
await readFile(filePath)
} catch (cause) {
throw new Error('', { cause, filePath: '/path' })
}
The onCreate()
option can be used to validate and transform error
parameters
.
modernErrors({
onCreate(error, parameters) {
const { filePath } = parameters
if (typeof filePath !== 'string') {
throw new Error('filePath must be a string.')
}
const hasFilePath = filePath !== undefined
Object.assign(error, { filePath, hasFilePath })
},
})
const error = new InputError('Could not read the file.', {
filePath: '/path',
unknownParam: true,
})
console.log(error.filePath) // '/path'
console.log(error.hasFilePath) // true
console.log(error.unknownParam) // undefined
The onCreate()
option can trigger error type-specific logic.
modernErrors({
onCreate(error, parameters) {
onCreateError[error.name](error, parameters)
},
})
const onCreateError = {
InputError(error, parameters) {
// ...
},
AuthError(error, parameters) {
// ...
},
// ...
}
The onCreate()
option can be used to set properties on all
instances of a given error type.
modernErrors({
onCreate(error, parameters) {
Object.assign(error, parameters, ERROR_PROPS[error.name])
},
})
const ERROR_PROPS = {
InputError: { isUser: true },
AuthError: { isUser: true },
DatabaseError: { isUser: false },
}
const error = new InputError('Could not read the file.')
console.log(error.isUser) // true
CLI applications can assign a different exit code and log verbosity per error
type by using handle-cli-error
.
#!/usr/bin/env node
import handleCliError from 'handle-cli-error'
// `programmaticMain()` must use `modern-errors`'s `errorHandler`
import programmaticMain from './main.js'
const cliMain = function () {
try {
const cliFlags = getCliFlags()
programmaticMain(cliFlags)
} catch (error) {
// Print `error` then exit the process
handleCliError(error, {
types: {
InputError: { exitCode: 1, short: true },
DatabaseError: { exitCode: 2, short: true },
default: { exitCode: 3 },
},
})
}
}
cliMain()
error.toJSON()
converts errors to plain objects that are
serializable to JSON
(or YAML,
etc.). It is
automatically called
by JSON.stringify()
. All error properties
are kept,
including
cause
.
The error
must be from a known type. However, any other
error (including Error
, TypeError
, RangeError
, etc.) is also serializable
providing it has been either passed to errorHandler()
, or
wrapped as an error.cause
.
try {
await readFile(filePath)
} catch (cause) {
const error = new InputError('Could not read the file.', {
cause,
filePath: '/path',
})
const errorObject = error.toJSON()
// {
// name: 'InputError',
// message: 'Could not read the file',
// stack: '...',
// cause: { name: 'Error', ... },
// filePath: '/path'
// }
const errorString = JSON.stringify(error)
// '{"name":"InputError",...}'
}
parse(errorObject)
converts those error plain objects back to
identical error instances.
The original error type is generically preserved. However, it is converted to a
generic Error
if it is neither a native type (TypeError
, RangeError
, etc.)
nor a known type.
const newErrorObject = JSON.parse(errorString)
const newError = parse(newErrorObject)
// InputError: Could not read the file.
// filePath: '/path'
// [cause]: Error: ...
Objects and arrays containing custom errors can be deeply serialized to JSON.
They can then be deeply parsed back using
JSON.parse()
's reviver.
const error = new InputError('Could not read the file.')
const deepObject = [{}, { error }]
const jsonString = JSON.stringify(deepObject)
const newDeepObject = JSON.parse(jsonString, (key, value) => parse(value))
console.log(newDeepObject[1].error) // InputError: Could not read the file.
This framework brings together a collection of modules which can also be used individually:
create-error-types
: Create multiple error typeserror-type
: Create one error typeerror-serializer
: Convert errors to/from plain objectsnormalize-exception
: Normalize exceptions/errorsmerge-error-cause
: Merge an error with itscause
error-cause-polyfill
: Polyfillerror.cause
handle-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!