index.cjs 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. // @ts-check
  2. /**
  3. * Light-weight helpers for handling JavaScript Errors in node.js and the
  4. * browser.
  5. */
  6. class OError extends Error {
  7. /**
  8. * The error that is the underlying cause of this error
  9. *
  10. * @type {unknown}
  11. */
  12. cause
  13. /**
  14. * List of errors encountered as the callback chain is unwound
  15. *
  16. * @type {TaggedError[] | undefined}
  17. */
  18. _oErrorTags
  19. /**
  20. * @param {string} message as for built-in Error
  21. * @param {Object} [info] extra data to attach to the error
  22. * @param {unknown} [cause] the internal error that caused this error
  23. */
  24. constructor(message, info, cause) {
  25. super(message)
  26. this.name = this.constructor.name
  27. if (info) this.info = info
  28. if (cause) this.cause = cause
  29. }
  30. /**
  31. * Set the extra info object for this error.
  32. *
  33. * @param {Object} info extra data to attach to the error
  34. * @return {this}
  35. */
  36. withInfo(info) {
  37. this.info = info
  38. return this
  39. }
  40. /**
  41. * Wrap the given error, which caused this error.
  42. *
  43. * @param {unknown} cause the internal error that caused this error
  44. * @return {this}
  45. */
  46. withCause(cause) {
  47. this.cause = cause
  48. return this
  49. }
  50. /**
  51. * Tag debugging information onto any error (whether an OError or not) and
  52. * return it.
  53. *
  54. * @example <caption>An error in a callback</caption>
  55. * function findUser(name, callback) {
  56. * fs.readFile('/etc/passwd', (err, data) => {
  57. * if (err) return callback(OError.tag(err, 'failed to read passwd'))
  58. * // ...
  59. * })
  60. * }
  61. *
  62. * @example <caption>A possible error in a callback</caption>
  63. * function cleanup(callback) {
  64. * fs.unlink('/tmp/scratch', (err) => callback(err && OError.tag(err)))
  65. * }
  66. *
  67. * @example <caption>An error with async/await</caption>
  68. * async function cleanup() {
  69. * try {
  70. * await fs.promises.unlink('/tmp/scratch')
  71. * } catch (err) {
  72. * throw OError.tag(err, 'failed to remove scratch file')
  73. * }
  74. * }
  75. *
  76. * @template {unknown} E
  77. * @param {E} error the error to tag
  78. * @param {string} [message] message with which to tag `error`
  79. * @param {Object} [info] extra data with wich to tag `error`
  80. * @return {E} the modified `error` argument
  81. */
  82. static tag(error, message, info) {
  83. const oError = /** @type {{ _oErrorTags: TaggedError[] | undefined }} */ (
  84. error
  85. )
  86. if (!oError._oErrorTags) oError._oErrorTags = []
  87. let tag
  88. if (Error.captureStackTrace) {
  89. // Hide this function in the stack trace, and avoid capturing it twice.
  90. tag = /** @type TaggedError */ ({ name: 'TaggedError', message, info })
  91. Error.captureStackTrace(tag, OError.tag)
  92. } else {
  93. tag = new TaggedError(message || '', info)
  94. }
  95. if (oError._oErrorTags.length >= OError.maxTags) {
  96. // Preserve the first tag and add an indicator that we dropped some tags.
  97. if (oError._oErrorTags[1] === DROPPED_TAGS_ERROR) {
  98. oError._oErrorTags.splice(2, 1)
  99. } else {
  100. oError._oErrorTags[1] = DROPPED_TAGS_ERROR
  101. }
  102. }
  103. oError._oErrorTags.push(tag)
  104. return error
  105. }
  106. /**
  107. * The merged info from any `tag`s and causes on the given error.
  108. *
  109. * If an info property is repeated, the last one wins.
  110. *
  111. * @param {unknown} error any error (may or may not be an `OError`)
  112. * @return {Object}
  113. */
  114. static getFullInfo(error) {
  115. const info = {}
  116. if (!error) return info
  117. const oError = /** @type{OError} */ (error)
  118. if (oError.cause) Object.assign(info, OError.getFullInfo(oError.cause))
  119. if (typeof oError.info === 'object') Object.assign(info, oError.info)
  120. if (oError._oErrorTags) {
  121. for (const tag of oError._oErrorTags) {
  122. Object.assign(info, tag.info)
  123. }
  124. }
  125. return info
  126. }
  127. /**
  128. * Return the `stack` property from `error`, including the `stack`s for any
  129. * tagged errors added with `OError.tag` and for any `cause`s.
  130. *
  131. * @param {unknown} error any error (may or may not be an `OError`)
  132. * @return {string}
  133. */
  134. static getFullStack(error) {
  135. if (!error) return ''
  136. const oError = /** @type{OError} */ (error)
  137. let stack = oError.stack || oError.message || '(no stack)'
  138. if (Array.isArray(oError._oErrorTags) && oError._oErrorTags.length) {
  139. stack += `\n${oError._oErrorTags.map(tag => tag.stack).join('\n')}`
  140. }
  141. const causeStack = OError.getFullStack(oError.cause)
  142. if (causeStack) {
  143. stack += '\ncaused by:\n' + indent(causeStack)
  144. }
  145. return stack
  146. }
  147. }
  148. /**
  149. * Maximum number of tags to apply to any one error instance. This is to avoid
  150. * a resource leak in the (hopefully unlikely) case that a singleton error
  151. * instance is returned to many callbacks. If tags have been dropped, the full
  152. * stack trace will include a placeholder tag `... dropped tags`.
  153. *
  154. * Defaults to 100. Must be at least 1.
  155. *
  156. * @type {Number}
  157. */
  158. OError.maxTags = 100
  159. /**
  160. * Used to record a stack trace every time we tag info onto an Error.
  161. *
  162. * @private
  163. * @extends OError
  164. */
  165. class TaggedError extends OError {}
  166. const DROPPED_TAGS_ERROR = /** @type{TaggedError} */ ({
  167. name: 'TaggedError',
  168. message: '... dropped tags',
  169. stack: 'TaggedError: ... dropped tags',
  170. })
  171. /**
  172. * @private
  173. * @param {string} string
  174. * @return {string}
  175. */
  176. function indent(string) {
  177. return string.replace(/^/gm, ' ')
  178. }
  179. module.exports = OError