index.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698
  1. /**
  2. * Module dependencies
  3. */
  4. var util = require('util');
  5. var _ = require('@sailshq/lodash');
  6. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  7. // Lodash methods in use:
  8. //
  9. // • Basic things:
  10. // ° _.isString, _.isObject, _.isArray, _.isError, _.isFunction, _.isNumber
  11. // • Node <=5 compat. things:
  12. // ° _.extend
  13. // • Stranger things:
  14. // ° _.matches (for .taste())
  15. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  16. /**
  17. * flaverr()
  18. *
  19. * Flavor an Error instance with the specified error code string or dictionary of customizations.
  20. *
  21. * Specifically, this modifies the provided Error instance either:
  22. * (A) by attaching a `code` property and setting it to the specified value (e.g. "E_USAGE"), or
  23. * (B) merging the specified dictionary of stuff into the Error
  24. *
  25. * If a `message` or `name` is provided, the Error instance's `stack` will be recalculated accordingly.
  26. * This can be used to consume an omen -- i.e. giving this Error instance's stack trace "hindsight",
  27. * and keeping it from getting "cliffed-out" on the wrong side of asynchronous callbacks.
  28. *
  29. * Besides improving the quality of your everyday errors and allowing for exception-based switching,
  30. * you can also use flaverr to build an _omen_, an Error instance defined ahead of time in order to
  31. * grab a stack trace. (used for providing a better experience when viewing the stack trace of errors
  32. * that come from one or more asynchronous ticks down the line; e.g. uniqueness errors.)
  33. *
  34. * > The "omen" approach is inspired by the implementation originally devised for Waterline:
  35. * > https://github.com/balderdashy/waterline/blob/6b1f65e77697c36561a0edd06dff537307986cb7/lib/waterline/utils/query/build-omen.js
  36. *
  37. * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  38. * @param {String|Dictionary} codeOrCustomizations
  39. * e.g. `"E_USAGE"`
  40. * -OR-
  41. * `{ name: 'UsageError', code: 'E_UHOH', machineInstance: foo, errors: [], misc: 'etc' }`
  42. *
  43. * @param {Error?} err
  44. * If `undefined`, a new Error will be instantiated instead.
  45. * e.g. `new Error('Invalid usage: That is not where the quarter is supposed to go.')`
  46. *
  47. * @param {Function?} caller
  48. * An optional function to use for context (useful for building omens)
  49. * The stack trace of the omen will be snipped based on the instruction where
  50. * this "caller" function was invoked.
  51. *
  52. * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  53. * @returns {Error}
  54. * An Error instance.
  55. * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  56. */
  57. function flaverr (codeOrCustomizations, err, caller){
  58. // Parse error (e.g. tolerate bluebird/stripe look-alikes)
  59. if (err !== undefined) {
  60. err = flaverr.parseError(err) || err;
  61. if (!_.isError(err)) {
  62. throw new Error('Unexpected usage. If specified, expected 2nd argument to be an Error instance (but instead got `'+util.inspect(err, {depth: null})+'`)');
  63. }
  64. }//fi
  65. if (caller !== undefined && typeof caller !== 'function') {
  66. throw new Error('Unexpected usage. If specified, expected 3rd argument should be a function that will be used as a stack trace context (but instead got `'+util.inspect(caller, {depth: null})+'`)');
  67. }
  68. if (_.isString(codeOrCustomizations)) {
  69. if (!err) {
  70. err = new Error();
  71. err.code = codeOrCustomizations;
  72. } else {
  73. err.code = codeOrCustomizations;
  74. }
  75. }
  76. else if (_.isObject(codeOrCustomizations) && !_.isArray(codeOrCustomizations) && typeof codeOrCustomizations !== 'function') {
  77. if (codeOrCustomizations.stack) { throw new Error('Unexpected usage. Customizations (dictionary provided as 1st arg) are not allowed to contain a `stack`. Instead, use `newErr = flaverr({ name: original.name, message: original.message }, omen)`'); }
  78. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  79. // FUTURE: (maybe? see traceFrom notes below)
  80. // if (codeOrCustomizations.stack) { throw new Error('Unexpected usage of `flaverr()`. Customizations (dictionary provided as 1st arg) are not allowed to contain a `stack`. Instead, use `flaverr.traceFrom(omen, err)`'); }
  81. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  82. if (!err){
  83. if (codeOrCustomizations.message !== undefined) {
  84. err = new Error(codeOrCustomizations.message);
  85. }
  86. else if (Object.keys(codeOrCustomizations).length === 0) {
  87. err = new Error();
  88. }
  89. else {
  90. err = new Error(util.inspect(codeOrCustomizations, {depth: 5}));
  91. }
  92. } else {
  93. if (codeOrCustomizations.name || codeOrCustomizations.message) {
  94. if (codeOrCustomizations.name === undefined) {
  95. codeOrCustomizations.name = err.name;
  96. }
  97. if (codeOrCustomizations.message === undefined) {
  98. codeOrCustomizations.message = err.message;
  99. }
  100. var numCharsToShift;
  101. if (err.message.length > 0) {
  102. // e.g. stack `FooBar: Blah blah blah\n at skdfgjagsd…`
  103. numCharsToShift = err.name.length + 2 + err.message.length;
  104. } else {
  105. // e.g. stack `FooBar\n at dudgjkadgsj…`
  106. numCharsToShift = err.name.length;
  107. }
  108. if (codeOrCustomizations.message.length > 0) {
  109. // e.g. stack `BazBot: Blurge blurge blurge\n at dudgjkadgsj…`
  110. err.stack = codeOrCustomizations.name + ': '+ codeOrCustomizations.message + err.stack.slice(numCharsToShift);
  111. } else {
  112. // e.g. stack `BazBot\n at dudgjkadgsj…`
  113. err.stack = codeOrCustomizations.name + err.stack.slice(numCharsToShift);
  114. }
  115. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  116. // FUTURE: Explore a fancier strategy like this (maybe):
  117. // ```
  118. // if (omen && omen._traceReference && Error.captureStackTrace) {
  119. // var omen2 = new Error(message);
  120. // Error.captureStackTrace(omen2, omen._traceReference);
  121. // omen2.name = name;
  122. // return omen;
  123. // }
  124. // ```
  125. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  126. }//fi
  127. }//fi
  128. // Always merge in the customizations, whether this is an existing error or a new one.
  129. _.extend(err, codeOrCustomizations);
  130. }
  131. else {
  132. throw new Error('Unexpected usage. Expected 1st argument to be either a string error code or a dictionary of customizations (but instead got `'+util.inspect(codeOrCustomizations, {depth: null})+'`)');
  133. }
  134. // If a `caller` reference was provided, then use it to adjust the stack trace.
  135. // (Note that we silently skip this step if the `Error.captureStackTrace` is missing
  136. // on the currently-running platform)
  137. if (caller && Error.captureStackTrace) {
  138. Error.captureStackTrace(err, caller);
  139. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  140. // FUTURE: maybe do something fancier here, or where this is called, to keep track of the omen so
  141. // that it can support both sorts of usages (Deferred and explicit callback.)
  142. //
  143. // This way, it could do an even better job of reporting exactly where the error came from in
  144. // userland code as the very first entry in the stack trace. e.g.
  145. // ```
  146. // Error.captureStackTrace(omen, Deferred.prototype.exec);
  147. // // ^^ but would need to pass through the original omen or something
  148. // ```
  149. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  150. }//fi
  151. return err;
  152. }//ƒ
  153. // Export flaverr.
  154. module.exports = flaverr;
  155. /**
  156. * flaverr.getBareTrace()
  157. *
  158. * Return the bare stack trace of an Error, with the identifying `name`/colon/space/`message`
  159. * preamble trimmed off, leaving only the info about stack frames.
  160. *
  161. * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  162. * Note:
  163. * While this function may have other uses, it was designed as a simple way of including
  164. * a stack trace in console warning messages.
  165. * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  166. *
  167. * @param {Error?|Function?} errOrCaller
  168. * If an Error is provided, its bare stack trace will be used.
  169. * Otherwise, a new Error will be instantiated on the fly and
  170. * its stack trace will be used. If a function is provided
  171. * instead of an Error, it is understood to be the caller and,
  172. * used for improving the trace of the newly instantiated Error
  173. * instance.
  174. *
  175. * @param {Number?} framesToKeep (««« YOU PROBABLY SHOULDN'T USE THIS!!)
  176. * The number of frames from the top of the call stack to retain
  177. * in the output trace. If omitted, all frames will be included.
  178. * | WARNING:
  179. * | It is almost NEVER a good idea to pass this argument in!!!
  180. * | Making programmatic assumptions about the call stack is
  181. * | notoriously difficult (and usually ill-advised), since it
  182. * | is something that can be impacted by many unexpected factors:
  183. * | from refactoring within your code base, interference from
  184. * | your dependencies, or even other runtime aspects or your
  185. * | users' environments that are completely out of your control.
  186. * | This warning applies both to app-level code AND for modules!
  187. * | In fact, the only reason this feature is even included in
  188. * | `flaverr` is for use during debugging, as well as (much more
  189. * | rarely) use cases for certain kinds of developer tooling.
  190. * | Proceed at your own risk. You have been warned!
  191. *
  192. * @returns {String}
  193. */
  194. module.exports.getBareTrace = function getBareTrace(errOrCaller, framesToKeep){
  195. var err;
  196. if (_.isError(errOrCaller)) {
  197. err = errOrCaller;
  198. } else if (_.isFunction(errOrCaller) || errOrCaller === undefined) {
  199. err = flaverr.buildOmen(errOrCaller||getBareTrace);
  200. } else {
  201. throw new Error('Unexpected usage of `.getBareTrace()`. If an argument is supplied, it must be an Error instance or function (the caller to use when generating a new trace). But instead, got: '+util.inspect(errOrCaller, {depth: 5}));
  202. }
  203. var numCharsToShift;
  204. if (err.message.length > 0) {
  205. // e.g. stack `FooBar: Blah blah blah\n at skdfgjagsd…`
  206. numCharsToShift = err.name.length + 2 + err.message.length;
  207. } else {
  208. // e.g. stack `FooBar\n at dudgjkadgsj…`
  209. numCharsToShift = err.name.length;
  210. }
  211. var bareTrace = err.stack;
  212. bareTrace = bareTrace.slice(numCharsToShift);
  213. bareTrace = bareTrace.replace(/^[\n]+/g,'');
  214. if (framesToKeep !== undefined) {
  215. if (!_.isNumber(framesToKeep) || framesToKeep < 1 || Math.floor(framesToKeep) !== framesToKeep) { throw new Error('Unexpected usage of `.getBareTrace()`. If a 2nd argument is supplied, it must be a positive whole integer (the # of stack frames to keep from the top of the call stack). But instead, got: '+util.inspect(framesToKeep, {depth: 5})); }
  216. // throw new Error('Unexpected usage of `getBareTrace()`. The 2nd argument is experimental and not yet supported for external use.');
  217. bareTrace = bareTrace.split(/\n/g).slice(0, framesToKeep).join('\n');
  218. }
  219. return bareTrace;
  220. };
  221. /**
  222. * buildOmen()
  223. *
  224. * Return a new Error instance for use as an omen.
  225. *
  226. * > Note: This isn't particularly slow, but it isn't speedy either.
  227. * > For VERY hot code paths, it's a good idea to skip this in production unless debugging.
  228. * > The standard way to do that is a userland check like:
  229. * > ```
  230. * > var omen = (process.env.NODE_ENV !== 'production' || process.env.DEBUG)?
  231. * > flaverr.buildOmen(theUserlandFunctionIAmInsideOf)
  232. * > :
  233. * > undefined;
  234. * > ```
  235. * >
  236. * > Then `.omen()` method (see below) is a shortcut for this.
  237. *
  238. * @param {Function?} caller
  239. * An optional function to use for context (useful for further improving stack traces)
  240. * The stack trace of the omen will be snipped based on the instruction where
  241. * this "caller" function was invoked. Otherwise, if omitted, `.buildOmen()` itself
  242. * will be considered the caller.
  243. *
  244. * @returns {Error}
  245. */
  246. module.exports.buildOmen = function buildOmen(caller) {
  247. if (caller !== undefined && !_.isFunction(caller)){ throw new Error('Unexpected usage: If an argument is supplied, it must be a function (the "caller", used for adjusting the stack trace). Instead, got: '+util.inspect(caller, {depth: 5})); }
  248. var omen = flaverr({}, new Error('omen'), caller||buildOmen);
  249. if (caller !== undefined && !omen.stack.match(/\n/)) {
  250. throw new Error('Unexpected behavior: When constructing omen using the specified "caller", the resulting Error\'s stack trace does not contain any newline characters. This usually means the provided "caller" isn\'t actually the caller at all (or at least it wasn\'t this time). Thus `Error.captureStackTrace()` failed to determine a trace. To resolve this, pass in a valid "caller" or if that\'s not possible, omit the "caller" argument altogether.');
  251. }
  252. return omen;
  253. };
  254. /**
  255. * omen()
  256. *
  257. * Do exactly the same thing as `.buildOmen()`, but instead of ALWAYS returning a new
  258. * Error instance, sometimes return `undefined` instead. (Useful for performance reasons.)
  259. *
  260. * @param {Function?} caller
  261. * (See `buildOmen()` for details.)
  262. *
  263. * @returns {Error?}
  264. * Error instance is returned UNLESS:
  265. * (A) the `NODE_ENV` system environment is set to "production", and
  266. * (B) the `DEBUG` system environment variable has not been set, or is falsey.
  267. * Otherwise, this returns `undefined`.
  268. */
  269. module.exports.omen = function omen(caller){
  270. return (process.env.NODE_ENV !== 'production' || process.env.DEBUG)?
  271. flaverr.buildOmen(caller||omen)
  272. :
  273. undefined;
  274. };
  275. /**
  276. * flaverr.parseError()
  277. *
  278. * Investigate the provided value and return a canonical Error instance,
  279. * if it is one itself, or if is is a recognized wrapper that contains one.
  280. *
  281. * > • If the provided value is a canonical Error instance already, then
  282. * > this just returns it as-is.
  283. * >
  284. * > • Otherwise, it attempts to tease out a canonical Error using a few
  285. * > different heuristics, including support for parsing special
  286. * > not-quite-Errors from bluebird.
  287. * >
  288. * > • If no existing Error instance can be squeezed out, then return `undefined`.
  289. * > (The `.parseError()` function **NEVER** constructs Error instances.)
  290. *
  291. * @param {Ref} err
  292. *
  293. * @returns {Error?}
  294. */
  295. module.exports.parseError = function(err) {
  296. if (_.isError(err)) {
  297. // > ok as-is
  298. return err;
  299. } else if (_.isObject(err) && err.cause && _.isError(err.cause)) {
  300. // Bluebird "errors"
  301. //
  302. // > async+await errors from bluebird are not necessarily "true" Error instances,
  303. // > as per _.isError() anyway (see https://github.com/node-machine/machine/commits/6b9d9590794e33307df1f7ba91e328dd236446a9).
  304. // > So to be reasonable, we have to be a bit more relaxed here and tolerate these
  305. // > sorts of "errors" directly as well (by tweezing out the `cause`, which is
  306. // > where the underlying Error instance actually lives.)
  307. return err.cause;
  308. } else if (_.isObject(err) && _.isString(err.type) && _.isString(err.message) && _.isString(err.stack)) {
  309. // Stripe SDK "errors"
  310. //
  311. // > The Stripe SDK likes to throw some not-quite-Error-instances.
  312. // > But they're close enough to use once you factor in a little integration
  313. // > code to adapt them. (Note that we have to remove the `stack`-- but we
  314. // > alias it separately as `_originalStack`.)
  315. var stripeErrorProps = _.omit(err, ['stack']);
  316. return flaverr(_.extend(stripeErrorProps, {
  317. name: err.type,
  318. _originalStack: err.stack
  319. }));
  320. } else {
  321. return undefined;
  322. }
  323. };
  324. /**
  325. * flaverr.parseOrBuildError()
  326. *
  327. * Investigate the provided value and attempt to parse out a canonical Error instance
  328. * using `flaverr.parseError()`. If that fails, construct a new Error instance (or
  329. * consume the omen, if one was provided) using the provided data as a basis for the
  330. * message, and then return that.
  331. *
  332. * > • If no canonical Error can be obtained, this function constructs a new
  333. * > Error instance, maintaining the provided value as `.raw` and making a
  334. * > simple, best-effort guess at an appropriate error message.
  335. *
  336. * @param {Ref} err
  337. * @param {Error?} omenForNewError
  338. * If `parseOrBuildError()` determines that it must construct a new Error instance,
  339. * then, if specified, this omen will be consumed instead of generating a new
  340. * Error.
  341. *
  342. * @returns {Error}
  343. *
  344. *
  345. * e.g.
  346. * ```
  347. * flaverr.unwrap('E_NON_ERROR', flaverr.parseOrBuildError('bleh'))
  348. * // => 'bleh'
  349. * ```
  350. */
  351. module.exports.parseOrBuildError = function(err, omenForNewError) {
  352. var parsedErr = flaverr.parseError(err);
  353. if (parsedErr) {
  354. return parsedErr;
  355. }
  356. else {
  357. // > build a new error, pretty-printing the original value as the `message`,
  358. // > or using it verbatim if it's a string. Note that we still attach the
  359. // > original value as `.raw` as well)
  360. return flaverr.wrap('E_NON_ERROR', err, omenForNewError||undefined);
  361. }
  362. };
  363. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  364. // FUTURE: (see note in .wrap() for reasoning)
  365. // (+ maybe we can come up with a snappier, but also non-ambiguous, name for this?
  366. // Basically this is for handling completely unknown/untrusted things that have been
  367. // sent back through a callback, rejected, or thrown from somewhere else.)
  368. //
  369. // (Maybe `.insulate()`? And its inverse)
  370. //
  371. // ```
  372. // module.exports.parseOrBuildThenWrapAndRetrace = function parseOrBuildThenWrapAndRetrace(raw, caller){
  373. // // Note that it's important for us to build an omen here, rather than accepting one as an argument!
  374. // // (Remember: This function is not intended for low-level use!)
  375. //
  376. // var omen = caller? flaverr.omen(caller) : flaverr.omen(parseOrBuildThenWrapAndRetrace);
  377. // var err = flaverr.parseError(raw);
  378. // if (err){
  379. // // If it's already a parseable Error, wrap it.
  380. // return flaverr.wrap('E_FROM_WITHIN', err, omen||undefined);
  381. // } else {
  382. // // Otherwise, it's some random thing so still wrap it, but use the special code
  383. // // for non-errors (i.e. just like in `parseOrBuildError()`)
  384. // return flaverr.wrap('E_NON_ERROR', raw, omen||undefined);
  385. // }
  386. // };
  387. // ```
  388. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  389. /**
  390. * flaverr.wrap()
  391. *
  392. * Build & return a new Envelope (Error instance), stuffing the provided data into its
  393. * `raw` property and flavoring the new envelope error with the provided customizations.
  394. *
  395. * > Note: Any Envelope is easily reversed back into its raw state (see `flaverr.unwrap()`).
  396. *
  397. * > WARNING: THIS IS DESIGNED FOR LOW-LEVEL USE IN FLOW CONTROL TOOLS LIKE PARLEY/MACHINE/ETC!
  398. * > (See .insulate() above for a simpler utility designed for more common, everyday tasks in userland.)
  399. *
  400. * ----------------------------------------------------------------------------------------------------
  401. * A few common envelope patterns come with recommended/conventional error codes:
  402. *
  403. * • E_ESCAPE_HATCH -- (Wraps an underlying Error instance in an easily-reversible fashion.)
  404. * Useful as a special Error for escaping from an outer `try` block.
  405. *
  406. * • E_NON_ERROR -- (Wraps some value other than an Error instance in an easily-reversible fashion.)
  407. * Useful as simple mechanism for encasing some value that is **not an Error instance** inside
  408. * a new Error instance.
  409. *
  410. * • E_FROM_WITHIN -- (Wraps something thrown/rejected/throwbacked from untrusted code)
  411. * Useful as a wrapper around some value that was thrown from untrusted code, and thus might
  412. * have properties (`.code`, `.name`, etc) that could conflict with programmatic error negotiation
  413. * elsewhere, or that would be otherwise confusing if unleashed on userland as-is. (This is
  414. * particularly useful for wrapping errors thrown from invocations of your procedural params;
  415. * e.g. in the `catch` block where you handle any error thrown by invoking a custom, user-specified
  416. * iteratee function.)
  417. *
  418. * Anything else can be generally assumed to be some mysterious internal error from a 3rd party
  419. * module/etc -- an error which may or may not be directly relevant to your users. In fact, it
  420. * may or may not even necessarily be an Error instance! (Regardless, it is accessible in `.raw`.)
  421. *
  422. * ----------------------------------------------------------------------------------------------------
  423. *
  424. * @param {String|Dictionary} codeOrCustomizations
  425. * Customizations for the Envelope. (Use `{}` to indicate no customizations.)
  426. *
  427. * @param {Ref} raw
  428. * The thing to wrap.
  429. *
  430. * @param {Error?} omen
  431. * An optional Error instance to consume instead of building a new Error instance.
  432. *
  433. * ----------------------------------------------------------------------------------------------------
  434. * @returns {Error}
  435. * The Envelope (either a new Error instance, or the modified version of the provided omen)
  436. */
  437. module.exports.wrap = function(codeOrCustomizations, raw, omen){
  438. // Note: This method always requires customizations, and is very careful to put `raw`
  439. // **untouched** into the new Envelope's `.raw` property. Similarly, `.unwrap()` helps
  440. // you retrieve this property from an Envelope untouched, through careful ducktyping.
  441. // This is good for low-level utilities that need to verify correctness, but not super
  442. // beautiful to use. See `.insulate()` above for a simpler, everyday utility.
  443. if (raw === undefined) { throw new Error('The 2nd argument to `flaverr.wrap()` (the raw thing to wrap) is mandatory.'); }
  444. if (omen !== undefined && !_.isError(omen)) { throw new Error('If provided, `omen` must be an Error instance to be used for improving stack traces.'); }
  445. var customizations;
  446. if (_.isString(codeOrCustomizations)) {
  447. customizations = {
  448. code: codeOrCustomizations
  449. };
  450. }
  451. else if (!_.isError(codeOrCustomizations) && _.isObject(codeOrCustomizations) && !_.isArray(codeOrCustomizations) && typeof codeOrCustomizations !== 'function') {
  452. customizations = codeOrCustomizations;
  453. if (customizations.name) {
  454. throw new Error('Invalid 1st argument to `flaverr.wrap()`: No need to provide a custom `name` for the Envelope-- the `.name` for errors returned by flaverr.wrap() is set in stone ("Envelope") and always attached automatically.');
  455. }
  456. }
  457. else {
  458. throw new Error('1st argument to `flaverr.wrap()` must be either a valid string to indicate the `.code` for the Envelope, or a dictionary of customizations to fold in (never an Error instance though!) But instead of one of those, got: '+util.inspect(codeOrCustomizations, {depth:5}));
  459. }
  460. return flaverr(_.extend({
  461. name: 'Envelope',
  462. message: _.isError(raw) ? raw.message : _.isString(raw) ? raw : util.inspect(raw, {depth: 5}),
  463. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  464. // ^ FUTURE: Better error message for the non-error case?
  465. // (see the `exits.error` impl in the machine runner for comparison,
  466. // and be sure to try any changes out by hand to experience the message
  467. // before deciding. It's definitely not cut and dry whether there should
  468. // even be a custom message in this case, or if just displaying the output
  469. // as the `message` -- like we currently do -- is more appropriate)
  470. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  471. raw: raw
  472. }, customizations), omen||undefined);
  473. };
  474. /**
  475. * flaverr.unwrap()
  476. *
  477. * Unwrap the provided Envelope, grabbing its "raw" property.
  478. *
  479. * Or, in some cases, just return the original 2nd argument as-is, if it is:
  480. * • a non-error
  481. * • an Error without `.name === 'Envelope'`
  482. * • an Error that doesn't match the provided negotiation rule
  483. *
  484. * @param {String|Dictionary} negotiationRule
  485. * A negotiation rule (e.g. like you'd use with flaverr.taste() or parley/bluebird's .catch() )
  486. *
  487. * @param {Ref?} envelopeOrSomething
  488. * The thing to try and potentially unwrap.
  489. *
  490. * @returns {Ref?}
  491. * EITHER the original, untouched `envelopeOrSomething` (if it wasn't an Envelope or wasn't a match)
  492. * OR the `.raw` stuff from inside of it (assuming it was an Envelop and it was a match)
  493. *
  494. * e.g.
  495. * ```
  496. * flaverr.unwrap('E_NON_ERROR', flaverr.parseOrBuildError(3))
  497. * // => 3
  498. *
  499. * flaverr.unwrap('E_NON_ERROR', 3)
  500. * // => 3
  501. *
  502. * flaverr.unwrap({}, flaverr.parseOrBuildError(3))
  503. * // => 3
  504. *
  505. * flaverr.unwrap({}, flaverr.wrap({}, 3))
  506. * // => 3
  507. *
  508. * flaverr.unwrap({code:'E_ESCAPE_HATCH', traceRef: self}, err)
  509. * // => …(the wrapped error)…
  510. *
  511. * flaverr.unwrap('E_NON_ERROR', flaverr.wrap('E_NON_ERROR', Infinity))
  512. * // => Infinity
  513. * ```
  514. */
  515. module.exports.unwrap = function(negotiationRule, envelopeOrSomething){
  516. if (negotiationRule === undefined) { throw new Error('Unexpected usage of `.unwrap()`. 1st argument (the negotiation rule, or "unwrap", to be check for) is mandatory.'); }
  517. if (!_.isString(negotiationRule) && (!_.isObject(negotiationRule) || _.isArray(negotiationRule) || _.isError(negotiationRule))) { throw new Error('Unexpected usage of `.unwrap()`. 1st argument (the negotiation rule, or "unwrap", to check for) must be a string or dictionary (aka plain JS object), like the kind you\'d use for a similar purpose in Lodash or bluebird. But instead, got: '+util.inspect(negotiationRule, {depth: 5})); }
  518. // First normalize (in case there are weird bluebird errors)
  519. var thingToUnwrap = flaverr.parseError(envelopeOrSomething) || envelopeOrSomething;
  520. if (!_.isError(thingToUnwrap) || thingToUnwrap.name !== 'Envelope' || !flaverr.taste(negotiationRule, thingToUnwrap)) {
  521. return thingToUnwrap;
  522. }
  523. else {
  524. return thingToUnwrap.raw;
  525. }
  526. };
  527. /**
  528. * flaverr.taste()
  529. *
  530. * Return whether the specified Error matches the given "taste" (aka negotiation rule).
  531. *
  532. * > (Uses _.matches() for dictionary comparison.)
  533. * > https://lodash.com/docs/3.10.1#matches
  534. *
  535. * @param {String|Dictionary} negotiationRule
  536. * @param {Ref} err
  537. * @return {Boolean}
  538. *
  539. * --
  540. *
  541. * > e.g.
  542. * > ```
  543. * > if (flaverr.taste('E_UNIQUE', 3822)){
  544. * > //…
  545. * > }
  546. * >
  547. * > if (flaverr.taste('E_UNIQUE', err)){
  548. * > //…
  549. * > }
  550. * >
  551. * > if (flaverr.taste({name:'AdapterError', code: 'E_UNIQUE'}, err)) {
  552. * > //…
  553. * > }
  554. * > ```
  555. */
  556. module.exports.taste = function(negotiationRule, err) {
  557. if (negotiationRule === undefined) { throw new Error('Unexpected usage of `.taste()`. 1st argument (the negotiation rule, or "taste", to be check for) is mandatory.'); }
  558. if (!_.isString(negotiationRule) && (!_.isObject(negotiationRule) || _.isArray(negotiationRule))) { throw new Error('Unexpected usage of `.taste()`. 1st argument (the negotiation rule, or "taste", to check for) must be a string or dictionary (aka plain JS object), like the kind you\'d use for a similar purpose in Lodash or bluebird. But instead, got: '+util.inspect(negotiationRule, {depth: 5})); }
  559. if (err === undefined) { throw new Error('Unexpected usage of `.taste()`. 2nd argument (the Error to taste) is mandatory.'); }
  560. // Non-errors always fail the taste test.
  561. if (!_.isError(err)) {
  562. return false;
  563. }
  564. if (_.isString(negotiationRule)) {
  565. return (err.code === negotiationRule);
  566. }
  567. else {
  568. return _.matches(negotiationRule)(err);
  569. }
  570. };
  571. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  572. // FUTURE: Additionally, this additional layer of wrapping could take care of improving
  573. // stack traces, even in the case where an Error comes up from inside the implementation.
  574. // If done carefully, this can be done in a way that protects characteristics of the
  575. // internal Error (e.g. its "code", etc.), while also providing a better stack trace.
  576. //
  577. // For example, something like this:
  578. // ```
  579. // /**
  580. // * flaverr.traceFrom()
  581. // *
  582. // * Return a modified version of the specified error.
  583. // *
  584. // */
  585. // module.exports.traceFrom = function (omen, err) {
  586. // var relevantPropNames = _.difference(
  587. // _.union(
  588. // ['name', 'message'],
  589. // Object.getOwnPropertyNames(err)
  590. // ),
  591. // ['stack']
  592. // );
  593. // var errTemplate = _.pick(err, relevantPropNames);
  594. // errTemplate.raw = err;//<< could override stuff-- that's ok (see below).
  595. // var _mainFlaverrFn = module.exports;
  596. // var newError = _mainFlaverrFn(errTemplate, omen);
  597. // return newError;
  598. // };
  599. // ```
  600. // > Note that, above, we also kept the original error (and thus _its_ trace) and
  601. // > attached that as a separate property. If the original error already has "raw",
  602. // > that's ok. This is one thing that it makes sense for us to mutate-- and any
  603. // > attempt to do otherwise would probably be more confusing (you can imagine a `while`
  604. // > loop where we add underscores in front of the string "raw" ad infinitum, and then
  605. // > eventually, when there are no conflicts, use that as a keyname. But again, that
  606. // > ends up being more confusing from a userland perspective.)
  607. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  608. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  609. // Why not `flaverr.try()`?
  610. //
  611. // Turns out it'd be a bit of a mess.
  612. //
  613. // More history / background:
  614. // https://gist.github.com/mikermcneil/c1bc2d57f5bedae810295e5ed8c5f935
  615. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -