index.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  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. if (oError._oErrorTags.length >= OError.maxTags) {
  82. // Preserve the first tag and add an indicator that we dropped some tags.
  83. if (oError._oErrorTags[1] === DROPPED_TAGS_ERROR) {
  84. oError._oErrorTags.splice(2, 1)
  85. } else {
  86. oError._oErrorTags[1] = DROPPED_TAGS_ERROR
  87. }
  88. }
  89. oError._oErrorTags.push(tag)
  90. return error
  91. }
  92. /**
  93. * The merged info from any `tag`s and causes on the given error.
  94. *
  95. * If an info property is repeated, the last one wins.
  96. *
  97. * @param {Error | null | undefined} error any error (may or may not be an `OError`)
  98. * @return {Object}
  99. */
  100. static getFullInfo(error) {
  101. const info = {}
  102. if (!error) return info
  103. const oError = /** @type{OError} */ (error)
  104. if (oError.cause) Object.assign(info, OError.getFullInfo(oError.cause))
  105. if (typeof oError.info === 'object') Object.assign(info, oError.info)
  106. if (oError._oErrorTags) {
  107. for (const tag of oError._oErrorTags) {
  108. Object.assign(info, tag.info)
  109. }
  110. }
  111. return info
  112. }
  113. /**
  114. * Return the `stack` property from `error`, including the `stack`s for any
  115. * tagged errors added with `OError.tag` and for any `cause`s.
  116. *
  117. * @param {Error | null | undefined} error any error (may or may not be an `OError`)
  118. * @return {string}
  119. */
  120. static getFullStack(error) {
  121. if (!error) return ''
  122. const oError = /** @type{OError} */ (error)
  123. let stack = oError.stack || oError.message || '(no stack)'
  124. if (Array.isArray(oError._oErrorTags) && oError._oErrorTags.length) {
  125. stack += `\n${oError._oErrorTags.map(tag => tag.stack).join('\n')}`
  126. }
  127. const causeStack = oError.cause && OError.getFullStack(oError.cause)
  128. if (causeStack) {
  129. stack += '\ncaused by:\n' + indent(causeStack)
  130. }
  131. return stack
  132. }
  133. }
  134. /**
  135. * Maximum number of tags to apply to any one error instance. This is to avoid
  136. * a resource leak in the (hopefully unlikely) case that a singleton error
  137. * instance is returned to many callbacks. If tags have been dropped, the full
  138. * stack trace will include a placeholder tag `... dropped tags`.
  139. *
  140. * Defaults to 100. Must be at least 1.
  141. *
  142. * @type {Number}
  143. */
  144. OError.maxTags = 100
  145. /**
  146. * Used to record a stack trace every time we tag info onto an Error.
  147. *
  148. * @private
  149. * @extends OError
  150. */
  151. class TaggedError extends OError {}
  152. const DROPPED_TAGS_ERROR = /** @type{TaggedError} */ ({
  153. name: 'TaggedError',
  154. message: '... dropped tags',
  155. stack: 'TaggedError: ... dropped tags',
  156. })
  157. /**
  158. * @private
  159. * @param {string} string
  160. * @return {string}
  161. */
  162. function indent(string) {
  163. return string.replace(/^/gm, ' ')
  164. }
  165. module.exports = OError