Skip to content

Latest commit

 

History

History
586 lines (439 loc) · 16 KB

README.md

File metadata and controls

586 lines (439 loc) · 16 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: ErrorType

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

errorHandler

Type: (anyException) => Error

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

URL where users should report internal errors/bugs.

onCreate

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.

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

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
}

Internal errors

Internal errors/bugs can be distinguished from user errors by:

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

Bug reports

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

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

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

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!