mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Improve tagging for a (maybe) null error (#12)
* Document a way of handling an error that might be null * Make typecheck strict * Test some edge cases Co-authored-by: Jakob Ackermann <jakob.ackermann@overleaf.com>
This commit is contained in:
parent
0cebce64c8
commit
ea2d76de51
7 changed files with 140 additions and 13 deletions
|
@ -366,7 +366,7 @@ Set the extra info object for this error.
|
||||||
|
|
||||||
| Param | Type | Description |
|
| Param | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| info | <code>Object</code> \| <code>null</code> \| <code>undefined</code> | extra data to attach to the error |
|
| info | <code>Object</code> | extra data to attach to the error |
|
||||||
|
|
||||||
<a name="OError+withCause"></a>
|
<a name="OError+withCause"></a>
|
||||||
|
|
||||||
|
@ -394,6 +394,31 @@ return it.
|
||||||
| [message] | <code>string</code> | message with which to tag `error` |
|
| [message] | <code>string</code> | message with which to tag `error` |
|
||||||
| [info] | <code>Object</code> | extra data with wich to tag `error` |
|
| [info] | <code>Object</code> | extra data with wich to tag `error` |
|
||||||
|
|
||||||
|
**Example** *(An error in a callback)*
|
||||||
|
```js
|
||||||
|
function findUser(name, callback) {
|
||||||
|
fs.readFile('/etc/passwd', (err, data) => {
|
||||||
|
if (err) return callback(OError.tag(err, 'failed to read passwd'))
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Example** *(A possible error in a callback)*
|
||||||
|
```js
|
||||||
|
function cleanup(callback) {
|
||||||
|
fs.unlink('/tmp/scratch', (err) => callback(err && OError.tag(err)))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Example** *(An error with async/await)*
|
||||||
|
```js
|
||||||
|
async function cleanup() {
|
||||||
|
try {
|
||||||
|
await fs.promises.unlink('/tmp/scratch')
|
||||||
|
} catch (err) {
|
||||||
|
throw OError.tag(err, 'failed to remove scratch file')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
<a name="OError.getFullInfo"></a>
|
<a name="OError.getFullInfo"></a>
|
||||||
|
|
||||||
### OError.getFullInfo(error) ⇒ <code>Object</code>
|
### OError.getFullInfo(error) ⇒ <code>Object</code>
|
||||||
|
|
26
libraries/o-error/index.d.ts
vendored
26
libraries/o-error/index.d.ts
vendored
|
@ -8,6 +8,28 @@ declare class OError extends Error {
|
||||||
* Tag debugging information onto any error (whether an OError or not) and
|
* Tag debugging information onto any error (whether an OError or not) and
|
||||||
* return it.
|
* 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')
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
* @param {Error} error the error to tag
|
* @param {Error} error the error to tag
|
||||||
* @param {string} [message] message with which to tag `error`
|
* @param {string} [message] message with which to tag `error`
|
||||||
* @param {Object} [info] extra data with wich to tag `error`
|
* @param {Object} [info] extra data with wich to tag `error`
|
||||||
|
@ -39,12 +61,12 @@ declare class OError extends Error {
|
||||||
constructor(message: string, info?: any, cause?: Error);
|
constructor(message: string, info?: any, cause?: Error);
|
||||||
info: any;
|
info: any;
|
||||||
cause: Error;
|
cause: Error;
|
||||||
/** @private @type {Array<TaggedError>} */
|
/** @private @type {Array<TaggedError> | undefined} */
|
||||||
private _oErrorTags;
|
private _oErrorTags;
|
||||||
/**
|
/**
|
||||||
* Set the extra info object for this error.
|
* Set the extra info object for this error.
|
||||||
*
|
*
|
||||||
* @param {Object | null | undefined} info extra data to attach to the error
|
* @param {Object} info extra data to attach to the error
|
||||||
* @return {this}
|
* @return {this}
|
||||||
*/
|
*/
|
||||||
withInfo(info: any): OError;
|
withInfo(info: any): OError;
|
||||||
|
|
|
@ -13,15 +13,14 @@ class OError extends Error {
|
||||||
this.name = this.constructor.name
|
this.name = this.constructor.name
|
||||||
if (info) this.info = info
|
if (info) this.info = info
|
||||||
if (cause) this.cause = cause
|
if (cause) this.cause = cause
|
||||||
|
/** @private @type {Array<TaggedError> | undefined} */
|
||||||
/** @private @type {Array<TaggedError>} */
|
|
||||||
this._oErrorTags // eslint-disable-line
|
this._oErrorTags // eslint-disable-line
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the extra info object for this error.
|
* Set the extra info object for this error.
|
||||||
*
|
*
|
||||||
* @param {Object | null | undefined} info extra data to attach to the error
|
* @param {Object} info extra data to attach to the error
|
||||||
* @return {this}
|
* @return {this}
|
||||||
*/
|
*/
|
||||||
withInfo(info) {
|
withInfo(info) {
|
||||||
|
@ -44,6 +43,28 @@ class OError extends Error {
|
||||||
* Tag debugging information onto any error (whether an OError or not) and
|
* Tag debugging information onto any error (whether an OError or not) and
|
||||||
* return it.
|
* 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')
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
* @param {Error} error the error to tag
|
* @param {Error} error the error to tag
|
||||||
* @param {string} [message] message with which to tag `error`
|
* @param {string} [message] message with which to tag `error`
|
||||||
* @param {Object} [info] extra data with wich to tag `error`
|
* @param {Object} [info] extra data with wich to tag `error`
|
||||||
|
@ -60,7 +81,7 @@ class OError extends Error {
|
||||||
tag = /** @type TaggedError */ ({ name: 'TaggedError', message, info })
|
tag = /** @type TaggedError */ ({ name: 'TaggedError', message, info })
|
||||||
Error.captureStackTrace(tag, OError.tag)
|
Error.captureStackTrace(tag, OError.tag)
|
||||||
} else {
|
} else {
|
||||||
tag = new TaggedError(message, info)
|
tag = new TaggedError(message || '', info)
|
||||||
}
|
}
|
||||||
|
|
||||||
oError._oErrorTags.push(tag)
|
oError._oErrorTags.push(tag)
|
||||||
|
@ -106,7 +127,7 @@ class OError extends Error {
|
||||||
|
|
||||||
const oError = /** @type{OError} */ (error)
|
const oError = /** @type{OError} */ (error)
|
||||||
|
|
||||||
let stack = oError.stack
|
let stack = oError.stack || '(no stack)'
|
||||||
|
|
||||||
if (Array.isArray(oError._oErrorTags) && oError._oErrorTags.length) {
|
if (Array.isArray(oError._oErrorTags) && oError._oErrorTags.length) {
|
||||||
stack += `\n${oError._oErrorTags.map((tag) => tag.stack).join('\n')}`
|
stack += `\n${oError._oErrorTags.map((tag) => tag.stack).join('\n')}`
|
||||||
|
@ -129,6 +150,11 @@ class OError extends Error {
|
||||||
*/
|
*/
|
||||||
class TaggedError extends OError {}
|
class TaggedError extends OError {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @param {string} string
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
function indent(string) {
|
function indent(string) {
|
||||||
return string.replace(/^/gm, ' ')
|
return string.replace(/^/gm, ' ')
|
||||||
}
|
}
|
||||||
|
|
6
libraries/o-error/package-lock.json
generated
6
libraries/o-error/package-lock.json
generated
|
@ -317,6 +317,12 @@
|
||||||
"integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==",
|
"integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/chai": {
|
||||||
|
"version": "4.2.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.12.tgz",
|
||||||
|
"integrity": "sha512-aN5IAC8QNtSUdQzxu7lGBgYAOuU1tmRU4c9dIq5OKGf/SBVjXo+ffM2wEjudAWbgpOhy60nLoAGH1xm8fpCKFQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/color-name": {
|
"@types/color-name": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
"update-readme": "doc/update-readme.js",
|
"update-readme": "doc/update-readme.js",
|
||||||
"test": "mocha",
|
"test": "mocha",
|
||||||
"test:coverage": "nyc --reporter=lcov --reporter=text-summary npm run test",
|
"test:coverage": "nyc --reporter=lcov --reporter=text-summary npm run test",
|
||||||
"typecheck": "tsc --allowJs --checkJs --noEmit --moduleResolution node --target ES6 *.js test/**/*.js",
|
"typecheck": "tsc --allowJs --checkJs --noEmit --moduleResolution node --strict --target ES6 *.js test/**/*.js",
|
||||||
"declaration:build": "rm -f index.d.ts && tsc --allowJs --declaration --emitDeclarationOnly --moduleResolution node --target ES6 index.js",
|
"declaration:build": "rm -f index.d.ts && tsc --allowJs --declaration --emitDeclarationOnly --moduleResolution node --target ES6 index.js",
|
||||||
"declaration:check": "git diff --exit-code -- index.d.ts",
|
"declaration:check": "git diff --exit-code -- index.d.ts",
|
||||||
"prepublishOnly": "npm run --silent declaration:build && npm run --silent declaration:check"
|
"prepublishOnly": "npm run --silent declaration:build && npm run --silent declaration:check"
|
||||||
|
@ -31,6 +31,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "github:overleaf/o-error",
|
"repository": "github:overleaf/o-error",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/chai": "^4.2.12",
|
||||||
"@types/node": "^13.13.2",
|
"@types/node": "^13.13.2",
|
||||||
"chai": "^3.3.0",
|
"chai": "^3.3.0",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
|
|
|
@ -149,9 +149,44 @@ describe('OError.tag', function () {
|
||||||
expect.fail('should have yielded an error')
|
expect.fail('should have yielded an error')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('is not included in the stack trace if using capture', function () {
|
||||||
|
if (!Error.captureStackTrace) return this.skip()
|
||||||
|
const err = new Error('test error')
|
||||||
|
OError.tag(err, 'test message')
|
||||||
|
const stack = OError.getFullStack(err)
|
||||||
|
expect(stack).to.match(/TaggedError: test message\n\s+at/)
|
||||||
|
expect(stack).to.not.match(/TaggedError: test message\n\s+at [\w.]*tag/)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('without Error.captureStackTrace', function () {
|
||||||
|
/* eslint-disable mocha/no-hooks-for-single-case */
|
||||||
|
before(function () {
|
||||||
|
this.originalCaptureStackTrace = Error.captureStackTrace
|
||||||
|
Error.captureStackTrace = null
|
||||||
|
})
|
||||||
|
after(function () {
|
||||||
|
Error.captureStackTrace = this.originalCaptureStackTrace
|
||||||
|
})
|
||||||
|
|
||||||
|
it('still captures a stack trace, albeit including itself', function () {
|
||||||
|
const err = new Error('test error')
|
||||||
|
OError.tag(err, 'test message')
|
||||||
|
expectFullStackWithoutStackFramesToEqual(err, [
|
||||||
|
'Error: test error',
|
||||||
|
'TaggedError: test message',
|
||||||
|
])
|
||||||
|
const stack = OError.getFullStack(err)
|
||||||
|
expect(stack).to.match(/TaggedError: test message\n\s+at [\w.]*tag/)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('OError.getFullInfo', function () {
|
describe('OError.getFullInfo', function () {
|
||||||
|
it('works when given null', function () {
|
||||||
|
expect(OError.getFullInfo(null)).to.deep.equal({})
|
||||||
|
})
|
||||||
|
|
||||||
it('works on a normal error', function () {
|
it('works on a normal error', function () {
|
||||||
const err = new Error('foo')
|
const err = new Error('foo')
|
||||||
expect(OError.getFullInfo(err)).to.deep.equal({})
|
expect(OError.getFullInfo(err)).to.deep.equal({})
|
||||||
|
@ -193,6 +228,10 @@ describe('OError.getFullInfo', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('OError.getFullStack', function () {
|
describe('OError.getFullStack', function () {
|
||||||
|
it('works when given null', function () {
|
||||||
|
expect(OError.getFullStack(null)).to.equal('')
|
||||||
|
})
|
||||||
|
|
||||||
it('works on a normal error', function () {
|
it('works on a normal error', function () {
|
||||||
const err = new Error('foo')
|
const err = new Error('foo')
|
||||||
const fullStack = OError.getFullStack(err)
|
const fullStack = OError.getFullStack(err)
|
||||||
|
|
|
@ -2,6 +2,10 @@ const { expect } = require('chai')
|
||||||
|
|
||||||
const OError = require('../..')
|
const OError = require('../..')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Error} e
|
||||||
|
* @param {any} expected
|
||||||
|
*/
|
||||||
exports.expectError = function OErrorExpectError(e, expected) {
|
exports.expectError = function OErrorExpectError(e, expected) {
|
||||||
expect(
|
expect(
|
||||||
e.name,
|
e.name,
|
||||||
|
@ -23,24 +27,28 @@ exports.expectError = function OErrorExpectError(e, expected) {
|
||||||
'error should be recognised by util.types.isNativeError'
|
'error should be recognised by util.types.isNativeError'
|
||||||
).to.be.true
|
).to.be.true
|
||||||
|
|
||||||
expect(e.stack, 'error should have a stack trace').to.be.truthy
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
e.toString(),
|
e.toString(),
|
||||||
'toString should return the default error message formatting'
|
'toString should return the default error message formatting'
|
||||||
).to.equal(expected.message)
|
).to.equal(expected.message)
|
||||||
|
|
||||||
|
expect(e.stack, 'error should have a stack trace').to.not.be.empty
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
e.stack.split('\n')[0],
|
/** @type {string} */ (e.stack).split('\n')[0],
|
||||||
'stack should start with the default error message formatting'
|
'stack should start with the default error message formatting'
|
||||||
).to.match(new RegExp(`^${expected.name}:`))
|
).to.match(new RegExp(`^${expected.name}:`))
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
e.stack.split('\n')[1],
|
/** @type {string} */ (e.stack).split('\n')[1],
|
||||||
'first stack frame should be the function where the error was thrown'
|
'first stack frame should be the function where the error was thrown'
|
||||||
).to.match(expected.firstFrameRx)
|
).to.match(expected.firstFrameRx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Error} error
|
||||||
|
* @param {String[]} expected
|
||||||
|
*/
|
||||||
exports.expectFullStackWithoutStackFramesToEqual = function (error, expected) {
|
exports.expectFullStackWithoutStackFramesToEqual = function (error, expected) {
|
||||||
const fullStack = OError.getFullStack(error)
|
const fullStack = OError.getFullStack(error)
|
||||||
const fullStackWithoutFrames = fullStack
|
const fullStackWithoutFrames = fullStack
|
||||||
|
|
Loading…
Reference in a new issue