o-error-util.test.js 12 KB

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