o-error-util.test.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  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. })
  138. describe('OError.getFullInfo', function () {
  139. it('works on a normal error', function () {
  140. const err = new Error('foo')
  141. expect(OError.getFullInfo(err)).to.deep.equal({})
  142. })
  143. it('works on an error with tags', function () {
  144. const err = OError.tag(new Error('foo'), 'bar', { userId: 123 })
  145. expect(OError.getFullInfo(err)).to.deep.equal({ userId: 123 })
  146. })
  147. it('merges info from an error and its tags', function () {
  148. const err = new OError('foo').withInfo({ projectId: 456 })
  149. OError.tag(err, 'failed to foo', { userId: 123 })
  150. expect(OError.getFullInfo(err)).to.deep.equal({
  151. projectId: 456,
  152. userId: 123,
  153. })
  154. })
  155. it('does not merge info from a cause', function () {
  156. const err1 = new Error('foo')
  157. const err2 = new Error('bar')
  158. err1.cause = err2
  159. err2.info = { userId: 123 }
  160. expect(OError.getFullInfo(err1)).to.deep.equal({})
  161. })
  162. it('merges info from tags with duplicate keys', function () {
  163. const err1 = OError.tag(new Error('foo'), 'bar', { userId: 123 })
  164. const err2 = OError.tag(err1, 'bat', { userId: 456 })
  165. expect(OError.getFullInfo(err2)).to.deep.equal({ userId: 456 })
  166. })
  167. it('works on an error with .info set to a string', function () {
  168. const err = new Error('foo')
  169. err.info = 'test'
  170. expect(OError.getFullInfo(err)).to.deep.equal({})
  171. })
  172. })
  173. describe('OError.getFullStack', function () {
  174. it('works on a normal error', function () {
  175. const err = new Error('foo')
  176. const fullStack = OError.getFullStack(err)
  177. expect(fullStack).to.match(/^Error: foo$/m)
  178. expect(fullStack).to.match(/^\s+at /m)
  179. })
  180. it('works on an error with a cause', function () {
  181. const err1 = new Error('foo')
  182. const err2 = new Error('bar')
  183. err1.cause = err2
  184. const fullStack = OError.getFullStack(err1)
  185. expect(fullStack).to.match(/^Error: foo$/m)
  186. expect(fullStack).to.match(/^\s+at /m)
  187. expect(fullStack).to.match(/^caused by:\n\s+Error: bar$/m)
  188. })
  189. it('works on both tags and causes', async function () {
  190. // Here's the actual error.
  191. function tryToFoo() {
  192. try {
  193. throw Error('foo')
  194. } catch (error) {
  195. throw OError.tag(error, 'failed to foo', { foo: 1 })
  196. }
  197. }
  198. // Inside another function that wraps it.
  199. function tryToBar() {
  200. try {
  201. tryToFoo()
  202. } catch (error) {
  203. throw new OError('failed to bar').withCause(error)
  204. }
  205. }
  206. // And it is in another try.
  207. try {
  208. try {
  209. tryToBar()
  210. expect.fail('should have thrown')
  211. } catch (error) {
  212. throw OError.tag(error, 'failed to bat', { bat: 1 })
  213. }
  214. } catch (error) {
  215. // We catch the wrapping error.
  216. expectError(error, {
  217. name: 'OError',
  218. klass: OError,
  219. message: 'OError: failed to bar',
  220. firstFrameRx: /tryToBar/,
  221. })
  222. // But the stack contains all of the errors and tags.
  223. expectFullStackWithoutStackFramesToEqual(error, [
  224. 'OError: failed to bar',
  225. 'TaggedError: failed to bat',
  226. 'caused by:',
  227. ' Error: foo',
  228. ' TaggedError: failed to foo',
  229. ])
  230. // The info from the wrapped cause should not leak out.
  231. expect(OError.getFullInfo(error)).to.eql({ bat: 1 })
  232. // But it should still be recorded.
  233. expect(OError.getFullInfo(error.cause)).to.eql({ foo: 1 })
  234. }
  235. })
  236. })