index.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. /**
  2. * Light-weight helpers for handling JavaScript Errors in node.js and the
  3. * browser.
  4. */
  5. class OError extends Error {
  6. /**
  7. * @param {string} message as for built-in Error
  8. * @param {Object} [info] extra data to attach to the error
  9. * @param {Error} [cause] the internal error that caused this error
  10. */
  11. constructor(message, info, cause) {
  12. super(message)
  13. this.name = this.constructor.name
  14. if (info) this.info = info
  15. if (cause) this.cause = cause
  16. /** @private @type {Array<TaggedError> | undefined} */
  17. this._oErrorTags // eslint-disable-line
  18. }
  19. /**
  20. * Set the extra info object for this error.
  21. *
  22. * @param {Object} info extra data to attach to the error
  23. * @return {this}
  24. */
  25. withInfo(info) {
  26. this.info = info
  27. return this
  28. }
  29. /**
  30. * Wrap the given error, which caused this error.
  31. *
  32. * @param {Error} cause the internal error that caused this error
  33. * @return {this}
  34. */
  35. withCause(cause) {
  36. this.cause = cause
  37. return this
  38. }
  39. /**
  40. * Tag debugging information onto any error (whether an OError or not) and
  41. * return it.
  42. *
  43. * @example <caption>An error in a callback</caption>
  44. * function findUser(name, callback) {
  45. * fs.readFile('/etc/passwd', (err, data) => {
  46. * if (err) return callback(OError.tag(err, 'failed to read passwd'))
  47. * // ...
  48. * })
  49. * }
  50. *
  51. * @example <caption>A possible error in a callback</caption>
  52. * function cleanup(callback) {
  53. * fs.unlink('/tmp/scratch', (err) => callback(err && OError.tag(err)))
  54. * }
  55. *
  56. * @example <caption>An error with async/await</caption>
  57. * async function cleanup() {
  58. * try {
  59. * await fs.promises.unlink('/tmp/scratch')
  60. * } catch (err) {
  61. * throw OError.tag(err, 'failed to remove scratch file')
  62. * }
  63. * }
  64. *
  65. * @param {Error} error the error to tag
  66. * @param {string} [message] message with which to tag `error`
  67. * @param {Object} [info] extra data with wich to tag `error`
  68. * @return {Error} the modified `error` argument
  69. */
  70. static tag(error, message, info) {
  71. const oError = /** @type{OError} */ (error)
  72. if (!oError._oErrorTags) oError._oErrorTags = []
  73. let tag
  74. if (Error.captureStackTrace) {
  75. // Hide this function in the stack trace, and avoid capturing it twice.
  76. tag = /** @type TaggedError */ ({ name: 'TaggedError', message, info })
  77. Error.captureStackTrace(tag, OError.tag)
  78. } else {
  79. tag = new TaggedError(message || '', info)
  80. }
  81. oError._oErrorTags.push(tag)
  82. return error
  83. }
  84. /**
  85. * The merged info from any `tag`s on the given error.
  86. *
  87. * If an info property is repeated, the last one wins.
  88. *
  89. * @param {Error | null | undefined} error any errror (may or may not be an `OError`)
  90. * @return {Object}
  91. */
  92. static getFullInfo(error) {
  93. const info = {}
  94. if (!error) return info
  95. const oError = /** @type{OError} */ (error)
  96. if (typeof oError.info === 'object') Object.assign(info, oError.info)
  97. if (oError._oErrorTags) {
  98. for (const tag of oError._oErrorTags) {
  99. Object.assign(info, tag.info)
  100. }
  101. }
  102. return info
  103. }
  104. /**
  105. * Return the `stack` property from `error`, including the `stack`s for any
  106. * tagged errors added with `OError.tag` and for any `cause`s.
  107. *
  108. * @param {Error | null | undefined} error any error (may or may not be an `OError`)
  109. * @return {string}
  110. */
  111. static getFullStack(error) {
  112. if (!error) return ''
  113. const oError = /** @type{OError} */ (error)
  114. let stack = oError.stack || '(no stack)'
  115. if (Array.isArray(oError._oErrorTags) && oError._oErrorTags.length) {
  116. stack += `\n${oError._oErrorTags.map((tag) => tag.stack).join('\n')}`
  117. }
  118. const causeStack = oError.cause && OError.getFullStack(oError.cause)
  119. if (causeStack) {
  120. stack += '\ncaused by:\n' + indent(causeStack)
  121. }
  122. return stack
  123. }
  124. }
  125. /**
  126. * Used to record a stack trace every time we tag info onto an Error.
  127. *
  128. * @private
  129. * @extends OError
  130. */
  131. class TaggedError extends OError {}
  132. /**
  133. * @private
  134. * @param {string} string
  135. * @return {string}
  136. */
  137. function indent(string) {
  138. return string.replace(/^/gm, ' ')
  139. }
  140. module.exports = OError