JSPM

  • Created
  • Published
  • Downloads 6916
  • Score
    100M100P100Q116797F
  • License MIT

Handle errors like it's 2023 🔮

Package Exports

  • modern-errors

Readme

modern-errors logo

Codecov TypeScript Node Twitter Medium

Handle errors like it's 2023 🔮

Error handling framework that is pluggable, minimalist yet featureful.

Features

Plugins

Example

Create error classes.

import modernErrors from 'modern-errors'

// Base error class
export const AnyError = modernErrors()

export const UnknownError = AnyError.subclass('UnknownError')
export const InputError = AnyError.subclass('InputError')
export const AuthError = AnyError.subclass('AuthError')
export const DatabaseError = AnyError.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 an `Error` instance
  throw AnyError.normalize(error)
}

Use plugins.

import modernErrors from 'modern-errors'
import modernErrorsSerialize from 'modern-errors-serialize'

// Use a plugin to serialize errors as JSON
export const AnyError = modernErrors([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 = AnyError.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 is an ES module and must be loaded using an import or import() statement, not require().

API

modernErrors(plugins?, options?)

plugins: Plugin[]?
options: object?

Creates and returns AnyError.

Options:

AnyError

Base error class.

AnyError.subclass(name, options?)

name: string
options: object?
Return value: class extends AnyError {}

Creates and returns an error subclass. The first one must be named UnknownError.

Subclasses can also call ErrorClass.subclass() themselves.

Options:

AnyError.normalize(anyException)

anyException: any
Return value: AnyError

Normalizes invalid errors and assigns the UnknownError class to unknown errors.

new AnyError(message, options?)

message: string
options: object?
Return value: AnyError

Options:

Usage

⛑️ Error classes

Create error classes

// Base error class
export const AnyError = modernErrors()

// The first error class must be named "UnknownError"
export const UnknownError = AnyError.subclass('UnknownError')
export const InputError = AnyError.subclass('InputError')
export const AuthError = AnyError.subclass('AuthError')
export const DatabaseError = AnyError.subclass('DatabaseError')

Export error classes

Exporting and documenting error classes (including AnyError and UnknownError) allows consumers to check them. This also enables sharing error classes between modules.

Check error classes

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

// Unknown error (from a specific library)
if (error instanceof UnknownError) {
  // ...
}

// Any error (from a specific library)
if (error instanceof AnyError) {
  // ...
}

🏷️ 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 = AnyError.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's, unless the outer error's class is AnyError.

try {
  throw new AuthError('...')
} catch (cause) {
  // Now an InputError
  throw new InputError('...', { cause })
}
try {
  throw new AuthError('...')
} catch (cause) {
  // Still an AuthError
  throw new AnyError('...', { cause })
}

Wrap error options

The outer error's options (props and plugin options) replace the inner one's. If the outer error's class is AnyError, those are merged instead.

try {
  throw new AuthError('...', innerOptions)
} catch (cause) {
  // Options are now `outerOptions`. `innerOptions` are discarded.
  throw new InputError('...', { ...outerOptions, cause })
}
try {
  throw new AuthError('...', innerOptions)
} catch (cause) {
  // `outerOptions` are merged with `innerOptions`
  throw new AnyError('...', { ...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. AnyError.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 = AnyError.normalize(invalidError)
  // This works: 'Missing file path.'
  // `normalizedError` is an `Error` instance.
  console.log(normalizedError.message.trim())
}

Top-level error handler

Wrapping a module's main functions with AnyError.normalize() 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 AnyError.normalize(error)
  }
}

🐞 Unknown errors

Normalizing unknown errors

An error is unknown if its class was not created by AnyError.subclass(). This indicates an unexpected exception, usually a bug. AnyError.normalize() assigns the UnknownError class to any unknown error.

try {
  return regExp.test(value)
} catch (unknownError) {
  // Now an `UnknownError` instance
  throw AnyError.normalize(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 (unknownError) {
  // Now an `InputError` instance
  throw new InputError('Invalid regular expression:', { cause: unknownError })
}

Using plugins with unknown errors

AnyError.normalize() is required for unknown errors to use plugins.

try {
  return regExp.test(value)
} catch (unknownError) {
  unknownError.examplePluginMethod() // This throws

  const normalizedError = AnyError.normalize(unknownError)
  normalizedError.examplePluginMethod() // This works
}

🔧 Custom logic

Class custom logic

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

export const InputError = AnyError.subclass('InputError', {
  // The `class` must extend from `AnyError`
  custom: class extends AnyError {
    // If a `constructor` is defined, its parameters must be (message, options)
    // like `AnyError`
    constructor(message, options) {
      // Modifying `message` or `options` should be done before `super()`
      message += message.endsWith('.') ? '' : '.'

      // All arguments should be forwarded to `super()`, including any
      // custom `options` or additional `constructor` parameters
      super(message, options)

      // `name` is automatically added, so this is not necessary
      // this.name = 'InputError'
    }

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

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

Shared custom logic

ErrorClass.subclass() can be used to share logic between error classes.

const SharedError = AnyError.subclass('SharedError', {
  custom: class extends AnyError {
    // ...
  },
})

export const InputError = SharedError.subclass('InputError')
export const AuthError = SharedError.subclass('AuthError')

🔌 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 modernErrors()'s first argument.

npm install modern-errors-{pluginName}
import modernErrors from 'modern-errors'

import modernErrorsBugs from 'modern-errors-bugs'
import modernErrorsSerialize from 'modern-errors-serialize'

export const AnyError = modernErrors([modernErrorsBugs, modernErrorsSerialize])
// ...

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 AnyError = modernErrors(plugins, options)
export const SharedError = AnyError.subclass('SharedError', options)

export const InputError = SharedError.subclass('InputError')
export const AuthError = SharedError.subclass('AuthError')
export const InputError = AnyError.subclass('InputError', options)
  • A specific error: second argument to the error's constructor
throw new InputError('...', options)
  • A plugin method call: last argument, passing only that plugin's options
AnyError[methodName](...args, options[pluginName])
error[methodName](...args, options[pluginName])

Custom plugins

Please see the following documentation to create your own plugin.

🤓 TypeScript

Please see the following documentation for information about TypeScript types.

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!