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 (unhandled) internal errors from (handled) user errors
- Internal 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
Errorinstance, 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-errorsThis 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: ErrorType
Any error name passed as argument is returned as an error type.
Type: (anyException) => Error
Error handler that should wrap each main function.
Type: (errorObject) => Error
Convert an error plain object into an Error instance.
Type: string | URL
URL where users should report internal errors/bugs.
Type: (error, parameters) => void
Called on any new ErrorType('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.causedoes 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 === 'InternalError') {
// ...
}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
}Internal errors/bugs can be distinguished from user errors by:
- Handling any possible errors in
try {} catch {} - Re-throwing them with a known error type
The errorHandler() assigns the InternalError type to any
error with an unknown type.
const getUserId = function (user) {
return user.id
}
getUserId(null) // InternalError: Cannot read properties of null (reading 'id')If the bugsUrl option is used,
modernErrors({ bugsUrl: 'https://github.com/my-name/my-project/issues' })any internal error will include the following message.
Please report this bug at: https://github.com/my-name/my-project/issues
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) // undefinedThe 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) // trueCLI 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 custom errors to plain objects that are
always safe to
serialize with JSON
(or YAML,
etc.). All error properties
are kept,
including
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.
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 itscauseerror-cause-polyfill: Polyfillerror.causehandle-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!