Skip to content

Latest commit

 

History

History
611 lines (461 loc) · 17.1 KB

README.md

File metadata and controls

611 lines (461 loc) · 17.1 KB
modern-errors logo

Codecov TypeScript Node Twitter Medium

Handle errors like it's 2022 🔮

Error handling framework that is minimalist yet featureful.

Features

Example

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

Install

npm install modern-errors

This package is an ES module and must be loaded using an import or import() statement, not require().

API

modernErrors(errorNames, options?)

errorNames string[]
options object
Return value: object

Creates custom error types.

Return value

Any error type

Type: CustomErrorType

Any error name passed as argument is returned as an error type.

errorHandler

Type: (anyException) => CustomError

Error handler that should wrap each main function.

parse

Type: (errorObject) => Error

Convert an error plain object into an Error instance.

Options

bugsUrl

Type: string | URL | ((error: Error) => string | URL | void)

URL where users should report unknown errors.

onCreate

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.

Usage

Setup

Create custom error types

// error.js
import modernErrors from 'modern-errors'

export const { InputError, AuthError, DatabaseError, errorHandler, parse } =
  modernErrors(['InputError', 'AuthError', 'DatabaseError'])

Error handler

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

Throw errors

Simple errors

import { InputError } from './error.js'

const validateFilePath = function (filePath) {
  if (filePath === '') {
    throw new InputError('Missing file path.')
  }
}

Invalid errors

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

Re-throw errors

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:

Wrap error message

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.

Error types

Test error type

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') {
  // ...
}

Set error type

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
}

Unknown errors

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

Bug reports

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

Error properties

Set error properties

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'

Wrap error properties

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

Customize error parameters

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

Type-specific logic

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

Error type properties

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 errors

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

Serialization/parsing

Serialize

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

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

Deep serialization/parsing

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.

Modules

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

Related projects

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!