o-error-util.test.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. const { expect } = require('chai')
  2. const { promisify } = require('util')
  3. const OError = require('..')
  4. const {
  5. expectError,
  6. expectFullStackWithoutStackFramesToEqual,
  7. } = require('./support')
  8. describe('OError.tag', function () {
  9. it('tags errors thrown from an async function', async function () {
  10. const delay = promisify(setTimeout)
  11. async function foo() {
  12. await delay(10)
  13. throw new Error('foo error')
  14. }
  15. async function bar() {
  16. try {
  17. await foo()
  18. } catch (error) {
  19. throw OError.tag(error, 'failed to bar', { bar: 'baz' })
  20. }
  21. }
  22. async function baz() {
  23. try {
  24. await bar()
  25. } catch (error) {
  26. throw OError.tag(error, 'failed to baz', { baz: 'bat' })
  27. }
  28. }
  29. try {
  30. await baz()
  31. expect.fail('should have thrown')
  32. } catch (error) {
  33. expectError(error, {
  34. name: 'Error',
  35. klass: Error,
  36. message: 'Error: foo error',
  37. firstFrameRx: /at foo/,
  38. })
  39. expectFullStackWithoutStackFramesToEqual(error, [
  40. 'Error: foo error',
  41. 'TaggedError: failed to bar',
  42. 'TaggedError: failed to baz',
  43. ])
  44. expect(OError.getFullInfo(error)).to.eql({
  45. bar: 'baz',
  46. baz: 'bat',
  47. })
  48. }
  49. })
  50. it('tags errors thrown from a promise rejection', async function () {
  51. function foo() {
  52. return new Promise((resolve, reject) => {
  53. setTimeout(function () {
  54. reject(new Error('foo error'))
  55. }, 10)
  56. })
  57. }
  58. async function bar() {
  59. try {
  60. await foo()
  61. } catch (error) {
  62. throw OError.tag(error, 'failed to bar', { bar: 'baz' })
  63. }
  64. }
  65. async function baz() {
  66. try {
  67. await bar()
  68. } catch (error) {
  69. throw OError.tag(error, 'failed to baz', { baz: 'bat' })
  70. }
  71. }
  72. try {
  73. await baz()
  74. expect.fail('should have thrown')
  75. } catch (error) {
  76. expectError(error, {
  77. name: 'Error',
  78. klass: Error,
  79. message: 'Error: foo error',
  80. firstFrameRx: /_onTimeout/,
  81. })
  82. expectFullStackWithoutStackFramesToEqual(error, [
  83. 'Error: foo error',
  84. 'TaggedError: failed to bar',
  85. 'TaggedError: failed to baz',
  86. ])
  87. expect(OError.getFullInfo(error)).to.eql({
  88. bar: 'baz',
  89. baz: 'bat',
  90. })
  91. }
  92. })
  93. it('tags errors yielded through callbacks', function (done) {
  94. function foo(cb) {
  95. setTimeout(function () {
  96. cb(new Error('foo error'))
  97. }, 10)
  98. }
  99. function bar(cb) {
  100. foo(function (err) {
  101. if (err) {
  102. return cb(OError.tag(err, 'failed to bar', { bar: 'baz' }))
  103. }
  104. cb()
  105. })
  106. }
  107. function baz(cb) {
  108. bar(function (err) {
  109. if (err) {
  110. return cb(OError.tag(err, 'failed to baz', { baz: 'bat' }))
  111. }
  112. cb()
  113. })
  114. }
  115. baz(function (err) {
  116. if (err) {
  117. expectError(err, {
  118. name: 'Error',
  119. klass: Error,
  120. message: 'Error: foo error',
  121. firstFrameRx: /_onTimeout/,
  122. })
  123. expectFullStackWithoutStackFramesToEqual(err, [
  124. 'Error: foo error',
  125. 'TaggedError: failed to bar',
  126. 'TaggedError: failed to baz',
  127. ])
  128. expect(OError.getFullInfo(err)).to.eql({
  129. bar: 'baz',
  130. baz: 'bat',
  131. })
  132. return done()
  133. }
  134. expect.fail('should have yielded an error')
  135. })
  136. })
  137. it('is not included in the stack trace if using capture', function () {
  138. if (!Error.captureStackTrace) return this.skip()
  139. const err = new Error('test error')
  140. OError.tag(err, 'test message')
  141. const stack = OError.getFullStack(err)
  142. expect(stack).to.match(/TaggedError: test message\n\s+at/)
  143. expect(stack).to.not.match(/TaggedError: test message\n\s+at [\w.]*tag/)
  144. })
  145. describe('without Error.captureStackTrace', function () {
  146. /* eslint-disable mocha/no-hooks-for-single-case */
  147. before(function () {
  148. this.originalCaptureStackTrace = Error.captureStackTrace
  149. Error.captureStackTrace = null
  150. })
  151. after(function () {
  152. Error.captureStackTrace = this.originalCaptureStackTrace
  153. })
  154. it('still captures a stack trace, albeit including itself', function () {
  155. const err = new Error('test error')
  156. OError.tag(err, 'test message')
  157. expectFullStackWithoutStackFramesToEqual(err, [
  158. 'Error: test error',
  159. 'TaggedError: test message',
  160. ])
  161. const stack = OError.getFullStack(err)
  162. expect(stack).to.match(/TaggedError: test message\n\s+at [\w.]*tag/)
  163. })
  164. })
  165. describe('with limit on the number of tags', function () {
  166. before(function () {
  167. this.originalMaxTags = OError.maxTags
  168. OError.maxTags = 3
  169. })
  170. after(function () {
  171. OError.maxTags = this.originalMaxTags
  172. })
  173. it('should not tag more than that', function () {
  174. const err = new Error('test error')
  175. OError.tag(err, 'test message 1')
  176. OError.tag(err, 'test message 2')
  177. OError.tag(err, 'test message 3')
  178. OError.tag(err, 'test message 4')
  179. OError.tag(err, 'test message 5')
  180. expectFullStackWithoutStackFramesToEqual(err, [
  181. 'Error: test error',
  182. 'TaggedError: test message 1',
  183. 'TaggedError: ... dropped tags',
  184. 'TaggedError: test message 4',
  185. 'TaggedError: test message 5',
  186. ])
  187. })
  188. it('should handle deep recursion', async function () {
  189. async function recursiveAdd(n) {
  190. try {
  191. if (n === 0) throw new Error('deep error')
  192. const result = await recursiveAdd(n - 1)
  193. return result + 1
  194. } catch (err) {
  195. throw OError.tag(err, `at level ${n}`)
  196. }
  197. }
  198. try {
  199. await recursiveAdd(10)
  200. } catch (err) {
  201. expectFullStackWithoutStackFramesToEqual(err, [
  202. 'Error: deep error',
  203. 'TaggedError: at level 0',
  204. 'TaggedError: ... dropped tags',
  205. 'TaggedError: at level 9',
  206. 'TaggedError: at level 10',
  207. ])
  208. }
  209. })
  210. it('should handle a singleton error', function (done) {
  211. const err = new Error('singleton error')
  212. function endpoint(callback) {
  213. helper((err) => callback(err && OError.tag(err, 'in endpoint')))
  214. }
  215. function helper(callback) {
  216. libraryFunction((err) => callback(err && OError.tag(err, 'in helper')))
  217. }
  218. function libraryFunction(callback) {
  219. callback(err)
  220. }
  221. endpoint(() => {
  222. endpoint((err) => {
  223. expect(err).to.exist
  224. expectFullStackWithoutStackFramesToEqual(err, [
  225. 'Error: singleton error',
  226. 'TaggedError: in helper',
  227. 'TaggedError: ... dropped tags',
  228. 'TaggedError: in helper',
  229. 'TaggedError: in endpoint',
  230. ])
  231. done()
  232. })
  233. })
  234. })
  235. })
  236. })
  237. describe('OError.getFullInfo', function () {
  238. it('works when given null', function () {
  239. expect(OError.getFullInfo(null)).to.deep.equal({})
  240. })
  241. it('works on a normal error', function () {
  242. const err = new Error('foo')
  243. expect(OError.getFullInfo(err)).to.deep.equal({})
  244. })
  245. it('works on an error with tags', function () {
  246. const err = OError.tag(new Error('foo'), 'bar', { userId: 123 })
  247. expect(OError.getFullInfo(err)).to.deep.equal({ userId: 123 })
  248. })
  249. it('merges info from an error and its tags', function () {
  250. const err = new OError('foo').withInfo({ projectId: 456 })
  251. OError.tag(err, 'failed to foo', { userId: 123 })
  252. expect(OError.getFullInfo(err)).to.deep.equal({
  253. projectId: 456,
  254. userId: 123,
  255. })
  256. })
  257. it('does not merge info from a cause', function () {
  258. const err1 = new Error('foo')
  259. const err2 = new Error('bar')
  260. err1.cause = err2
  261. err2.info = { userId: 123 }
  262. expect(OError.getFullInfo(err1)).to.deep.equal({})
  263. })
  264. it('merges info from tags with duplicate keys', function () {
  265. const err1 = OError.tag(new Error('foo'), 'bar', { userId: 123 })
  266. const err2 = OError.tag(err1, 'bat', { userId: 456 })
  267. expect(OError.getFullInfo(err2)).to.deep.equal({ userId: 456 })
  268. })
  269. it('works on an error with .info set to a string', function () {
  270. const err = new Error('foo')
  271. err.info = 'test'
  272. expect(OError.getFullInfo(err)).to.deep.equal({})
  273. })
  274. })
  275. describe('OError.getFullStack', function () {
  276. it('works when given null', function () {
  277. expect(OError.getFullStack(null)).to.equal('')
  278. })
  279. it('works on a normal error', function () {
  280. const err = new Error('foo')
  281. const fullStack = OError.getFullStack(err)
  282. expect(fullStack).to.match(/^Error: foo$/m)
  283. expect(fullStack).to.match(/^\s+at /m)
  284. })
  285. it('works on an error with a cause', function () {
  286. const err1 = new Error('foo')
  287. const err2 = new Error('bar')
  288. err1.cause = err2
  289. const fullStack = OError.getFullStack(err1)
  290. expect(fullStack).to.match(/^Error: foo$/m)
  291. expect(fullStack).to.match(/^\s+at /m)
  292. expect(fullStack).to.match(/^caused by:\n\s+Error: bar$/m)
  293. })
  294. it('works on both tags and causes', async function () {
  295. // Here's the actual error.
  296. function tryToFoo() {
  297. try {
  298. throw Error('foo')
  299. } catch (error) {
  300. throw OError.tag(error, 'failed to foo', { foo: 1 })
  301. }
  302. }
  303. // Inside another function that wraps it.
  304. function tryToBar() {
  305. try {
  306. tryToFoo()
  307. } catch (error) {
  308. throw new OError('failed to bar').withCause(error)
  309. }
  310. }
  311. // And it is in another try.
  312. try {
  313. try {
  314. tryToBar()
  315. expect.fail('should have thrown')
  316. } catch (error) {
  317. throw OError.tag(error, 'failed to bat', { bat: 1 })
  318. }
  319. } catch (error) {
  320. // We catch the wrapping error.
  321. expectError(error, {
  322. name: 'OError',
  323. klass: OError,
  324. message: 'OError: failed to bar',
  325. firstFrameRx: /tryToBar/,
  326. })
  327. // But the stack contains all of the errors and tags.
  328. expectFullStackWithoutStackFramesToEqual(error, [
  329. 'OError: failed to bar',
  330. 'TaggedError: failed to bat',
  331. 'caused by:',
  332. ' Error: foo',
  333. ' TaggedError: failed to foo',
  334. ])
  335. // The info from the wrapped cause should not leak out.
  336. expect(OError.getFullInfo(error)).to.eql({ bat: 1 })
  337. // But it should still be recorded.
  338. expect(OError.getFullInfo(error.cause)).to.eql({ foo: 1 })
  339. }
  340. })
  341. })