merge-error-cause
    TypeScript icon, indicating that this package has built-in type declarations

    3.0.0 • Public • Published

    Codecov TypeScript Node Twitter Medium

    Merge an error with its cause.

    This merges error.cause recursively with its parent error, including its message, stack, name and errors.

    Example

    import mergeErrorCause from 'merge-error-cause'
    
    const main = function (userId) {
      try {
        return createUser(userId)
      } catch (error) {
        throw mergeErrorCause(error)
        // Printed as:
        //   TypeError: Invalid user id: false
        //   Could not create user.
      }
    }
    
    const createUser = function (userId) {
      try {
        validateUserId(userId)
        return sendDatabaseRequest('create', userId)
      } catch (cause) {
        throw new Error('Could not create user.', { cause })
      }
    }
    
    const validateUserId = function (userId) {
      if (typeof userId !== 'string') {
        throw new TypeError(`Invalid user id: ${userId}.`)
      }
    }
    
    main(false)

    Install

    npm install merge-error-cause

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

    API

    mergeErrorCause(error)

    error Error | any
    Return value: Error

    error is modified and returned.

    If error's class is Error or if error.wrap is true, error.cause is modified and returned instead.

    If error is not a valid Error, a new error is created and returned instead.

    This never throws.

    Background

    error.cause is a recent JavaScript feature to wrap error messages and properties.

    try {
      validateUserId(userId)
      sendDatabaseRequest('create', userId)
    } catch (cause) {
      throw new Error('Could not create user.', { cause })
    }

    However, it comes with a few issues.

    Traversing error.cause

    Problem

    Consumers need to traverse error.cause.

    try {
      createUser(userId)
    } catch (error) {
      if (error.code === 'E101' || (error.cause && error.cause.code === 'E101')) {
        // Checking for properties requires traversing `error.cause`
      }
    
      if (
        error.name === 'UserError' ||
        (error.cause && error.cause.name === 'UserError')
      ) {
        // So does checking for error class
      }
    }

    This is tricky to get right. For example:

    • error.cause.cause might also exist (and so on)
    • If error is not an Error instance, error.name might throw
    • Recursing over error.cause might be an infinite cycle

    Solution

    This library merges error.cause recursively. It also ensures error is an Error instance. Consumers can then handle errors without checking its cause.

    try {
      createUser(userId)
    } catch (error) {
      if (error.code === 'E101') {
        /* ... */
      }
    
      if (error.name === 'UserError') {
        /* ... */
      }
    }

    Verbose stack trace

    Problem

    Stack traces with multiple error.cause can be quite verbose.

    Error: Could not create user group.
        at createUserGroup (/home/user/app/user_group.js:19:9)
        at createGroups (/home/user/app/user_group.js:101:10)
        at startApp (/home/user/app/app.js:35:20)
        at main (/home/user/app/app.js:3:4) {
      [cause]: Error: Could not create user.
          at newUser (/home/user/app/user.js:52:7)
          at createUser (/home/user/app/user.js:43:5)
          at createUserGroup (/home/user/app/user_group.js:17:11)
          at createGroups (/home/user/app/user_group.js:101:10)
          at startApp (/home/user/app/app.js:35:20)
          at main (/home/user/app/app.js:3:4) {
        [cause]: Error: Invalid user.
            at validateUser (/home/user/app/user.js:159:8)
            at userInstance (/home/user/app/user.js:20:4)
            at newUser (/home/user/app/user.js:50:7)
            at createUser (/home/user/app/user.js:43:5)
            at createUserGroup (/home/user/app/user_group.js:17:11)
            at createGroups (/home/user/app/user_group.js:101:10)
            at startApp (/home/user/app/app.js:35:20)
            at main (/home/user/app/app.js:3:4) {
          [cause]: UserError: User "15" does not exist.
              at checkUserId (/home/user/app/user.js:195:3)
              at checkUserExist (/home/user/app/user.js:170:10)
              at validateUser (/home/user/app/user.js:157:23)
              at userInstance (/home/user/app/user.js:20:4)
              at newUser (/home/user/app/user.js:50:7)
              at createUser (/home/user/app/user.js:43:5)
              at createUserGroup (/home/user/app/user_group.js:17:11)
              at createGroups (/home/user/app/user_group.js:101:10)
              at startApp (/home/user/app/app.js:35:20)
              at main (/home/user/app/app.js:3:4)
        }
      }
    }
    

    Each error cause is indented and printed separately.

    • The stack traces mostly repeat each other since the function calls are part of the same line execution
    • The most relevant message (innermost) is harder to find since it is shown last

    Solution

    This library only keeps the innermost stack trace. Error messages are concatenated by default from innermost to outermost. This results in much simpler stack traces without losing any information.

    TypeError: User "15" does not exist.
    Invalid user.
    Could not create user.
    Could not create user group.
        at checkUserId (/home/user/app/user.js:195:3)
        at checkUserExist (/home/user/app/user.js:170:10)
        at validateUser (/home/user/app/user.js:157:23)
        at userInstance (/home/user/app/user.js:20:4)
        at newUser (/home/user/app/user.js:50:7)
        at createUser (/home/user/app/user.js:43:5)
        at createUserGroup (/home/user/app/user_group.js:17:11)
        at createGroups (/home/user/app/user_group.js:101:10)
        at startApp (/home/user/app/app.js:35:20)
        at main (/home/user/app/app.js:3:4)
    

    Features

    Stack traces

    Only the innermost stack trace is kept.

    Please make sure you use async/await instead of new Promise() or callbacks to prevent truncated stack traces.

    Messages

    Inner error messages are printed first.

    try {
      throw new Error('Invalid user id.')
    } catch (cause) {
      throw new Error('Could not create user.', { cause })
      // Printed as:
      //   Error: Invalid user id.
      //   Could not create user.
    }

    If the outer error message ends with :, it is prepended instead.

    try {
      throw new Error('Invalid user id.')
    } catch (cause) {
      throw new Error('Could not create user:', { cause })
      // Printed as:
      //   Error: Could not create user: Invalid user id.
    }

    : can optionally be followed by a newline.

    try {
      throw new Error('Invalid user id.')
    } catch (cause) {
      throw new Error('Could not create user:\n', { cause })
      // Printed as:
      //   Error: Could not create user:
      //   Invalid user id.
    }

    Error class

    The outer error class is used.

    try {
      throw new TypeError('User id is not a string.')
    } catch (cause) {
      const error = new UserError('Could not create user.', { cause })
      const mergedError = mergeErrorCause(error)
      console.log(mergedError instanceof UserError) // true
      console.log(mergedError.name) // 'UserError'
    }

    If the parent error class is Error, the child class is used instead. This allows wrapping the error message or properties while keeping its class.

    try {
      throw new TypeError('User id is not a string.')
    } catch (cause) {
      const error = new Error('Could not create user.', { cause })
      console.log(mergeErrorCause(error) instanceof TypeError) // true
    }

    error.wrap: true has the same effect, but works with any parent error class.

    try {
      throw new TypeError('User id is not a string.')
    } catch (cause) {
      const error = new UserError('Could not create user.', { cause })
      error.wrap = true
      console.log(mergeErrorCause(error) instanceof TypeError) // true
    }

    Error properties

    Error properties are shallowly merged.

    // Both `userId` and `invalidUser` are kept
    try {
      throw Object.assign(new Error('Invalid user id.'), { userId: '5' })
    } catch (cause) {
      throw Object.assign(new Error('Could not create user.', { cause }), {
        invalidUser: true,
      })
    }

    Empty error messages are ignored. This is useful when wrapping error properties.

    try {
      throw new Error('Invalid user id.')
    } catch (cause) {
      throw Object.assign(new Error('', { cause }), { invalidUser: true })
    }

    Aggregate errors

    Any aggregateError.errors[*].cause is processed recursively. However, aggregateError.errors are not merged with each other since those are different from each other.

    If both error.errors and error.cause.errors exist, they are concatenated.

    Normalization

    Invalid errors are normalized to proper Error instances.

    try {
      throw 'Invalid user id.'
    } catch (error) {
      console.log(mergeErrorCause(error)) // Error: Invalid user id.
    }

    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!

    Install

    npm i merge-error-cause

    DownloadsWeekly Downloads

    108

    Version

    3.0.0

    License

    Apache-2.0

    Unpacked Size

    32.7 kB

    Total Files

    10

    Last publish

    Collaborators

    • ehmicky