Skip to content

Latest commit

 

History

History
680 lines (504 loc) · 19.1 KB

README.md

File metadata and controls

680 lines (504 loc) · 19.1 KB
modern-errors logo

Node Browsers TypeScript Codecov Minified size Twitter Medium

Handle errors like it's 2023 🔮

Error handling framework that is pluggable, minimalist yet featureful.

Features

Plugins

Example

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')

Throw errors.

throw new InputError('Missing file path.')

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))

Install

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 >=14.18.0 and browsers. It is an ES module and must be loaded using an import or import() statement, not require().

Usage

⛑️ Error classes

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')

Export error classes

Exporting and documenting all error classes (except for ModernError) allows consumers to check them. This also enables sharing error classes between modules.

Check error classes

if (error instanceof InputError) {
  // ...
}

Error subclasses

ErrorClass.subclass() returns a subclass. Parent classes' options are merged to 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

🏷️ Throw errors

Simple errors

throw new InputError('Missing file path.')

Error instance properties

const error = new InputError('...', { props: { isUserError: true } })
console.log(error.isUserError) // true

Error class properties

const InputError = BaseError.subclass('InputError', {
  props: { isUserError: true },
})
const error = new InputError('...')
console.log(error.isUserError) // true

Aggregate errors

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: ...
//   ]
// }

🎀 Wrap errors

Wrap inner error

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 })
}

Wrap error message

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 })

Wrap error class

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 })
}

Wrap error options

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 })
}

🚨 Normalize errors

Wrapped errors

Any error can be directly passed to the cause option, even if it is invalid, unknown or not normalized.

try {
  // ...
} catch (cause) {
  throw new InputError('...', { cause })
}

Invalid errors

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())
}

Top-level error handler

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 = function () {
  try {
    // ...
  } catch (error) {
    throw BaseError.normalize(error, UnknownError)
  }
}

🐞 Unknown errors

Normalizing unknown errors

An error is unknown if its class is not a subclass of the BaseError. This indicates an unexpected exception, usually a bug.

BaseError.normalize(error, UnknownError) assigns the UnknownError class to those errors. This is usually performed in the top-level error handler.

export const UnknownError = BaseError.subclass('UnknownError')
try {
  return regExp.test(value)
} catch (error) {
  // Now an `UnknownError` instance
  throw BaseError.normalize(error, UnknownError)
}

Handling unknown errors

Unknown errors should be handled in a try {} catch {} block and wrapped with a known class instead. That block should only cover the statement that might throw in order to prevent catching other unrelated unknown errors.

try {
  return regExp.test(value)
} catch (error) {
  // Now an `InputError` instance
  throw new InputError('Invalid regular expression:', { cause: error })
}

🔌 Plugins

List of plugins

Plugins extend modern-errors features. All available plugins are listed here.

Adding plugins

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],
})
// ...

Custom plugins

Please see the following documentation to create your own plugin.

Plugin options

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):

export const BaseError = ModernError.subclass('BaseError', options)
export const InputError = BaseError.subclass('InputError', options)
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])

🔧 Custom logic

The custom option can be used to provide an error class with additional methods, constructor or properties.

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)
    constructor(message, options) {
      message += message.endsWith('.') ? '' : '.'
      super(message, options)
    }

    isUserInput() {
      // ...
    }
  },
})

const error = new InputError('Wrong user name')
console.log(error.message) // 'Wrong user name.'
console.log(error.isUserInput())

🤓 TypeScript

Please see the following documentation for information about TypeScript types.

API

ModernError

Top-level ErrorClass.

ErrorClass.subclass(name, options?)

name: string
options: ClassOptions?

Creates and returns a child ErrorClass.

options

options.props

Type: object

Error class properties.

options.plugins

Type: Plugin[]

options.custom

Type: class extends ErrorClass {}

Custom class to add any methods, constructor or properties.

options.*

Any plugin options can also be specified.

new ErrorClass(message, options?)

message: string
options: InstanceOptions?
Return value: Error

options

options.props

Type: object

Error instance properties.

options.cause

Type: any

Inner error being wrapped.

options.errors

Type: any[]

Array of errors being aggregated.

options.*

Any plugin options can also be specified.

ErrorClass.normalize(error, NewErrorClass?)

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.

Modules

This framework brings together a collection of modules which can also be used individually:

Support

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.

Contributing

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!