o-error-util.test.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  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 when given a string', function () {
  243. const err = 'not an error instance'
  244. expect(OError.getFullInfo(err)).to.deep.equal({})
  245. })
  246. it('works on a normal error', function () {
  247. const err = new Error('foo')
  248. expect(OError.getFullInfo(err)).to.deep.equal({})
  249. })
  250. it('works on an error with tags', function () {
  251. const err = OError.tag(new Error('foo'), 'bar', { userId: 123 })
  252. expect(OError.getFullInfo(err)).to.deep.equal({ userId: 123 })
  253. })
  254. it('merges info from an error and its tags', function () {
  255. const err = new OError('foo').withInfo({ projectId: 456 })
  256. OError.tag(err, 'failed to foo', { userId: 123 })
  257. expect(OError.getFullInfo(err)).to.deep.equal({
  258. projectId: 456,
  259. userId: 123,
  260. })
  261. })
  262. it('merges info from a cause', function () {
  263. const err1 = new Error('foo')
  264. const err2 = new Error('bar')
  265. err1.cause = err2
  266. err2.info = { userId: 123 }
  267. expect(OError.getFullInfo(err1)).to.deep.equal({ userId: 123 })
  268. })
  269. it('merges info from a nested cause', function () {
  270. const err1 = new Error('foo')
  271. const err2 = new Error('bar')
  272. const err3 = new Error('baz')
  273. err1.cause = err2
  274. err2.info = { userId: 123 }
  275. err2.cause = err3
  276. err3.info = { foo: 42 }
  277. expect(OError.getFullInfo(err1)).to.deep.equal({
  278. userId: 123,
  279. foo: 42,
  280. })
  281. })
  282. it('merges info from cause with duplicate keys', function () {
  283. const err1 = new Error('foo')
  284. const err2 = new Error('bar')
  285. err1.info = { userId: 42, foo: 1337 }
  286. err1.cause = err2
  287. err2.info = { userId: 1 }
  288. expect(OError.getFullInfo(err1)).to.deep.equal({
  289. userId: 42,
  290. foo: 1337,
  291. })
  292. })
  293. it('merges info from tags with duplicate keys', function () {
  294. const err1 = OError.tag(new Error('foo'), 'bar', { userId: 123 })
  295. const err2 = OError.tag(err1, 'bat', { userId: 456 })
  296. expect(OError.getFullInfo(err2)).to.deep.equal({ userId: 456 })
  297. })
  298. it('works on an error with .info set to a string', function () {
  299. const err = new Error('foo')
  300. err.info = 'test'
  301. expect(OError.getFullInfo(err)).to.deep.equal({})
  302. })
  303. })
  304. describe('OError.getFullStack', function () {
  305. it('works when given null', function () {
  306. expect(OError.getFullStack(null)).to.equal('')
  307. })
  308. it('works on a normal error', function () {
  309. const err = new Error('foo')
  310. const fullStack = OError.getFullStack(err)
  311. expect(fullStack).to.match(/^Error: foo$/m)
  312. expect(fullStack).to.match(/^\s+at /m)
  313. })
  314. it('works on an error with a cause', function () {
  315. const err1 = new Error('foo')
  316. const err2 = new Error('bar')
  317. err1.cause = err2
  318. const fullStack = OError.getFullStack(err1)
  319. expect(fullStack).to.match(/^Error: foo$/m)
  320. expect(fullStack).to.match(/^\s+at /m)
  321. expect(fullStack).to.match(/^caused by:\n\s+Error: bar$/m)
  322. })
  323. it('works on both tags and causes', async function () {
  324. // Here's the actual error.
  325. function tryToFoo() {
  326. try {
  327. throw Error('foo')
  328. } catch (error) {
  329. throw OError.tag(error, 'failed to foo', { foo: 1 })
  330. }
  331. }
  332. // Inside another function that wraps it.
  333. function tryToBar() {
  334. try {
  335. tryToFoo()
  336. } catch (error) {
  337. throw new OError('failed to bar').withCause(error)
  338. }
  339. }
  340. // And it is in another try.
  341. try {
  342. try {
  343. tryToBar()
  344. expect.fail('should have thrown')
  345. } catch (error) {
  346. throw OError.tag(error, 'failed to bat', { bat: 1 })
  347. }
  348. } catch (error) {
  349. // We catch the wrapping error.
  350. expectError(error, {
  351. name: 'OError',
  352. klass: OError,
  353. message: 'OError: failed to bar',
  354. firstFrameRx: /tryToBar/,
  355. })
  356. // But the stack contains all of the errors and tags.
  357. expectFullStackWithoutStackFramesToEqual(error, [
  358. 'OError: failed to bar',
  359. 'TaggedError: failed to bat',
  360. 'caused by:',
  361. ' Error: foo',
  362. ' TaggedError: failed to foo',
  363. ])
  364. // The info from the wrapped cause should be picked up for logging.
  365. expect(OError.getFullInfo(error)).to.eql({ bat: 1, foo: 1 })
  366. // But it should still be recorded.
  367. expect(OError.getFullInfo(error.cause)).to.eql({ foo: 1 })
  368. }
  369. })
  370. it('works when given non Error', function () {
  371. expect(OError.getFullStack({ message: 'Foo' })).to.equal('Foo')
  372. })
  373. it('works when given non Error with tags', function () {
  374. const error = OError.tag({ message: 'Foo: bar' }, 'baz')
  375. expectFullStackWithoutStackFramesToEqual(error, [
  376. 'Foo: bar',
  377. 'TaggedError: baz',
  378. ])
  379. })
  380. })
  381. })