overleaf/libraries/o-error/index.js

191 lines
5.1 KiB
JavaScript
Raw Normal View History

/**
2020-04-28 15:40:20 -04:00
* Light-weight helpers for handling JavaScript Errors in node.js and the
2020-04-29 15:58:10 -04:00
* browser.
*/
2019-07-05 09:24:20 -04:00
class OError extends Error {
/**
* @param {string} message as for built-in Error
2020-04-28 15:40:20 -04:00
* @param {Object} [info] extra data to attach to the error
2020-04-29 15:58:10 -04:00
* @param {Error} [cause] the internal error that caused this error
*/
2020-04-28 15:40:20 -04:00
constructor(message, info, cause) {
super(message)
this.name = this.constructor.name
2020-04-28 15:40:20 -04:00
if (info) this.info = info
if (cause) this.cause = cause
/** @private @type {Array<TaggedError> | undefined} */
2020-04-28 15:40:20 -04:00
this._oErrorTags // eslint-disable-line
}
/**
* Set the extra info object for this error.
*
* @param {Object} info extra data to attach to the error
2020-04-28 15:40:20 -04:00
* @return {this}
*/
withInfo(info) {
this.info = info
return this
}
/**
* Wrap the given error, which caused this error.
*
2020-04-29 15:58:10 -04:00
* @param {Error} cause the internal error that caused this error
* @return {this}
*/
2020-04-17 03:44:50 -04:00
withCause(cause) {
this.cause = cause
return this
}
2020-04-28 15:40:20 -04:00
/**
* Tag debugging information onto any error (whether an OError or not) and
* return it.
*
* @example <caption>An error in a callback</caption>
* function findUser(name, callback) {
* fs.readFile('/etc/passwd', (err, data) => {
* if (err) return callback(OError.tag(err, 'failed to read passwd'))
* // ...
* })
* }
*
* @example <caption>A possible error in a callback</caption>
* function cleanup(callback) {
* fs.unlink('/tmp/scratch', (err) => callback(err && OError.tag(err)))
* }
*
* @example <caption>An error with async/await</caption>
* async function cleanup() {
* try {
* await fs.promises.unlink('/tmp/scratch')
* } catch (err) {
* throw OError.tag(err, 'failed to remove scratch file')
* }
* }
*
2020-04-29 15:58:10 -04:00
* @param {Error} error the error to tag
* @param {string} [message] message with which to tag `error`
* @param {Object} [info] extra data with wich to tag `error`
2020-04-28 15:40:20 -04:00
* @return {Error} the modified `error` argument
*/
static tag(error, message, info) {
const oError = /** @type{OError} */ (error)
2020-04-28 15:40:20 -04:00
if (!oError._oErrorTags) oError._oErrorTags = []
let tag
if (Error.captureStackTrace) {
2020-05-05 04:06:15 -04:00
// Hide this function in the stack trace, and avoid capturing it twice.
tag = /** @type TaggedError */ ({ name: 'TaggedError', message, info })
Error.captureStackTrace(tag, OError.tag)
} else {
tag = new TaggedError(message || '', info)
}
2020-04-28 15:40:20 -04:00
if (oError._oErrorTags.length >= OError.maxTags) {
// Preserve the first tag and add an indicator that we dropped some tags.
if (oError._oErrorTags[1] === DROPPED_TAGS_ERROR) {
oError._oErrorTags.splice(2, 1)
} else {
oError._oErrorTags[1] = DROPPED_TAGS_ERROR
}
}
2020-04-28 15:40:20 -04:00
oError._oErrorTags.push(tag)
return error
}
/**
* The merged info from any `tag`s and causes on the given error.
2020-04-28 15:40:20 -04:00
*
* If an info property is repeated, the last one wins.
*
* @param {Error | null | undefined} error any error (may or may not be an `OError`)
2020-04-28 15:40:20 -04:00
* @return {Object}
*/
static getFullInfo(error) {
const info = {}
if (!error) return info
const oError = /** @type{OError} */ (error)
if (oError.cause) Object.assign(info, OError.getFullInfo(oError.cause))
2020-04-28 15:40:20 -04:00
if (typeof oError.info === 'object') Object.assign(info, oError.info)
if (oError._oErrorTags) {
for (const tag of oError._oErrorTags) {
Object.assign(info, tag.info)
}
}
return info
}
/**
* Return the `stack` property from `error`, including the `stack`s for any
* tagged errors added with `OError.tag` and for any `cause`s.
*
2020-04-29 15:58:10 -04:00
* @param {Error | null | undefined} error any error (may or may not be an `OError`)
2020-04-28 15:40:20 -04:00
* @return {string}
*/
static getFullStack(error) {
if (!error) return ''
const oError = /** @type{OError} */ (error)
let stack = oError.stack || oError.message || '(no stack)'
2020-04-28 15:40:20 -04:00
2020-05-15 06:07:08 -04:00
if (Array.isArray(oError._oErrorTags) && oError._oErrorTags.length) {
stack += `\n${oError._oErrorTags.map(tag => tag.stack).join('\n')}`
2020-04-28 15:40:20 -04:00
}
const causeStack = oError.cause && OError.getFullStack(oError.cause)
if (causeStack) {
stack += '\ncaused by:\n' + indent(causeStack)
}
return stack
}
}
/**
* Maximum number of tags to apply to any one error instance. This is to avoid
* a resource leak in the (hopefully unlikely) case that a singleton error
* instance is returned to many callbacks. If tags have been dropped, the full
* stack trace will include a placeholder tag `... dropped tags`.
*
* Defaults to 100. Must be at least 1.
*
* @type {Number}
*/
OError.maxTags = 100
/**
2020-04-28 15:40:20 -04:00
* Used to record a stack trace every time we tag info onto an Error.
*
2020-04-28 15:40:20 -04:00
* @private
* @extends OError
*/
2020-04-28 15:40:20 -04:00
class TaggedError extends OError {}
const DROPPED_TAGS_ERROR = /** @type{TaggedError} */ ({
name: 'TaggedError',
message: '... dropped tags',
stack: 'TaggedError: ... dropped tags',
})
/**
* @private
* @param {string} string
* @return {string}
*/
2020-04-28 15:40:20 -04:00
function indent(string) {
return string.replace(/^/gm, ' ')
}
2019-07-05 12:45:43 -04:00
module.exports = OError