mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #2 from overleaf/es6
Add support for errors based on ES6 classes
This commit is contained in:
commit
5bf235c549
8 changed files with 1420 additions and 186 deletions
|
@ -1,12 +1,104 @@
|
|||
# overleaf-error-type
|
||||
|
||||
Make custom error types that pass `instanceof` checks, have stack traces and support custom messages and properties.
|
||||
Make custom error types that:
|
||||
- pass `instanceof` checks,
|
||||
- have stack traces,
|
||||
- support custom messages and properties (`info`), and
|
||||
- can wrap internal errors (causes) like [VError](https://github.com/joyent/node-verror).
|
||||
|
||||
## For ES6
|
||||
|
||||
ES6 classes make it easy to define custom errors by subclassing `Error`. Subclassing `errorType.Error` adds a few extra helpers.
|
||||
|
||||
### Usage
|
||||
|
||||
#### Throw an error directly
|
||||
|
||||
```js
|
||||
const errorType = require('overleaf-error-type')
|
||||
|
||||
function doSomethingBad () {
|
||||
throw new errorType.Error({
|
||||
message: 'did something bad',
|
||||
info: { thing: 'foo' }
|
||||
})
|
||||
}
|
||||
doSomethingBad()
|
||||
// =>
|
||||
// { ErrorTypeError: did something bad
|
||||
// at doSomethingBad (repl:2:9) <-- stack trace
|
||||
// name: 'ErrorTypeError', <-- default name
|
||||
// info: { thing: 'foo' } } <-- attached info
|
||||
```
|
||||
|
||||
#### Custom error class
|
||||
|
||||
```js
|
||||
class FooError extends errorType.Error {
|
||||
constructor (options) {
|
||||
super({ message: 'failed to foo', ...options })
|
||||
}
|
||||
}
|
||||
|
||||
function doFoo () {
|
||||
throw new FooError({ info: { foo: 'bar' } })
|
||||
}
|
||||
doFoo()
|
||||
// =>
|
||||
// { FooError: failed to foo
|
||||
// at doFoo (repl:2:9) <-- stack trace
|
||||
// name: 'FooError', <-- correct name
|
||||
// info: { foo: 'bar' } } <-- attached info
|
||||
```
|
||||
|
||||
#### Wrapping an inner error (cause)
|
||||
|
||||
```js
|
||||
function doFoo2 () {
|
||||
try {
|
||||
throw new Error('bad')
|
||||
} catch (err) {
|
||||
throw new FooError({ info: { foo: 'bar' } }).withCause(err)
|
||||
}
|
||||
}
|
||||
|
||||
doFoo2()
|
||||
// =>
|
||||
// { FooError: failed to foo: bad <-- combined message
|
||||
// at doFoo2 (repl:5:11) <-- stack trace
|
||||
// name: 'FooError', <-- correct name
|
||||
// info: { foo: 'bar' }, <-- attached info
|
||||
// cause: <-- the cause (inner error)
|
||||
// Error: bad <-- inner error message
|
||||
// at doFoo2 (repl:3:11) <-- inner error stack trace
|
||||
// at repl:1:1
|
||||
// ...
|
||||
|
||||
try {
|
||||
doFoo2()
|
||||
} catch (err) {
|
||||
console.log(errorType.getFullStack(err))
|
||||
}
|
||||
// =>
|
||||
// FooError: failed to foo: bad
|
||||
// at doFoo2 (repl:5:11)
|
||||
// at repl:2:3
|
||||
// ...
|
||||
// caused by: Error: bad
|
||||
// at doFoo2 (repl:3:11)
|
||||
// at repl:2:3
|
||||
// ...
|
||||
```
|
||||
|
||||
## For ES5
|
||||
|
||||
For backward compatibility, the following ES5-only interface is still supported.
|
||||
|
||||
The approach is based mainly on https://gist.github.com/justmoon/15511f92e5216fa2624b; it just tries to DRY it up a bit.
|
||||
|
||||
## Usage
|
||||
### Usage
|
||||
|
||||
### Define a standalone error class
|
||||
#### Define a standalone error class
|
||||
|
||||
```js
|
||||
const errorType = require('overleaf-error-type')
|
||||
|
@ -22,7 +114,7 @@ doSomethingBad()
|
|||
// at doSomethingBad (repl:2:9) <-- stack trace
|
||||
```
|
||||
|
||||
### Define an error subclass
|
||||
#### Define an error subclass
|
||||
|
||||
```js
|
||||
const SubCustomError = errorType.extend(CustomError, 'SubCustomError')
|
||||
|
@ -37,7 +129,7 @@ try {
|
|||
}
|
||||
```
|
||||
|
||||
### Add custom message and/or properties
|
||||
#### Add custom message and/or properties
|
||||
|
||||
```js
|
||||
const UserNotFoundError = errorType.define('UserNotFoundError',
|
||||
|
@ -50,7 +142,7 @@ throw new UserNotFoundError(123)
|
|||
// => UserNotFoundError: User not found: 123
|
||||
```
|
||||
|
||||
### Add custom Error types under an existing class
|
||||
#### Add custom Error types under an existing class
|
||||
|
||||
```js
|
||||
class User {
|
||||
|
@ -72,5 +164,17 @@ User.lookup(123)
|
|||
|
||||
## References
|
||||
|
||||
General:
|
||||
|
||||
- [MDN: Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)
|
||||
- [Error Handling in Node.js](https://www.joyent.com/node-js/production/design/errors)
|
||||
- [verror](https://github.com/joyent/node-verror)
|
||||
|
||||
For ES6:
|
||||
|
||||
- [Custom JavaScript Errors in ES6](https://medium.com/@xjamundx/custom-javascript-errors-in-es6-aa891b173f87)
|
||||
- [Custom errors, extending Error](https://javascript.info/custom-errors)
|
||||
|
||||
For ES5:
|
||||
|
||||
- https://gist.github.com/justmoon/15511f92e5216fa2624b
|
||||
- [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)
|
||||
|
|
|
@ -3,11 +3,119 @@
|
|||
var util = require('util')
|
||||
|
||||
/**
|
||||
* Make custom error types that pass `instanceof` checks, have stack traces and
|
||||
* support custom messages and properties.
|
||||
* Make custom error types that pass `instanceof` checks, have stack traces,
|
||||
* support custom messages and properties, and support wrapping errors (causes).
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
//
|
||||
// For ES6+ Classes
|
||||
//
|
||||
|
||||
/**
|
||||
* A base class for custom errors that handles:
|
||||
*
|
||||
* 1. Wrapping an optional 'cause'.
|
||||
* 2. Storing an 'info' object with additional data.
|
||||
* 3. Setting the name to the subclass name.
|
||||
*
|
||||
* @extends Error
|
||||
*/
|
||||
class ErrorTypeError extends Error {
|
||||
/**
|
||||
* @param {string} message as for built-in Error
|
||||
* @param {?object} info extra data to attach to the error
|
||||
*/
|
||||
constructor ({ message, info }) {
|
||||
super(message)
|
||||
this.name = this.constructor.name
|
||||
this.info = info
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the given error, which caused this error.
|
||||
*
|
||||
* @param {Error} cause
|
||||
* @return {this}
|
||||
*/
|
||||
withCause (cause) {
|
||||
this.cause = cause
|
||||
if (this.message && cause.message) {
|
||||
this.message += ': ' + cause.message
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for errors with a corresponding HTTP status code.
|
||||
*
|
||||
* @extends ErrorTypeError
|
||||
*/
|
||||
class ErrorWithStatusCode extends ErrorTypeError {
|
||||
/**
|
||||
* @param {?number} statusCode an HTTP status code
|
||||
* @param {object} options as for ErrorTypeError
|
||||
*/
|
||||
constructor ({ statusCode, ...options }) {
|
||||
super(options)
|
||||
this.statusCode = statusCode || 500
|
||||
}
|
||||
}
|
||||
exports.ErrorWithStatusCode = ErrorWithStatusCode
|
||||
|
||||
/**
|
||||
* Return the `info` property from `error` and recursively merge the `info`
|
||||
* properties from the error's causes, if any.
|
||||
*
|
||||
* If a property is repeated, the first one in the cause chain wins.
|
||||
*
|
||||
* @param {?Error} error assumed not to have circular causes
|
||||
* @return {Object}
|
||||
*/
|
||||
function getFullInfo (error) {
|
||||
if (!error) return {}
|
||||
const info = getFullInfo(error.cause)
|
||||
if (typeof error.info === 'object') Object.assign(info, error.info)
|
||||
return info
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the `stack` property from `error` and recursively append the `stack`
|
||||
* properties from the error's causes, if any.
|
||||
*
|
||||
* @param {?Error} error assumed not to have circular causes
|
||||
* @return {string}
|
||||
*/
|
||||
function getFullStack (error) {
|
||||
if (!error) return ''
|
||||
const causeStack = getFullStack(error.cause)
|
||||
if (causeStack) return (error.stack + '\ncaused by: ' + causeStack)
|
||||
return error.stack
|
||||
}
|
||||
|
||||
/**
|
||||
* Is `error` or one of its causes an instance of `klass`?
|
||||
*
|
||||
* @param {?Error} error assumed not to have circular causes
|
||||
* @param {function} klass
|
||||
* @return {Boolean}
|
||||
*/
|
||||
function hasCauseInstanceOf (error, klass) {
|
||||
if (!error) return false
|
||||
return error instanceof klass || hasCauseInstanceOf(error.cause, klass)
|
||||
}
|
||||
|
||||
exports.Error = ErrorTypeError
|
||||
exports.getFullInfo = getFullInfo
|
||||
exports.getFullStack = getFullStack
|
||||
exports.hasCauseInstanceOf = hasCauseInstanceOf
|
||||
|
||||
//
|
||||
// For ES5
|
||||
//
|
||||
|
||||
function extendErrorType (base, name, builder) {
|
||||
var errorConstructor = function () {
|
||||
Error.captureStackTrace && Error.captureStackTrace(this, this.constructor)
|
||||
|
|
1112
libraries/o-error/npm-shrinkwrap.json
generated
1112
libraries/o-error/npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load diff
|
@ -11,6 +11,6 @@
|
|||
"private": true,
|
||||
"devDependencies": {
|
||||
"chai": "^3.3.0",
|
||||
"mocha": "^2.3.3"
|
||||
"mocha": "^6.1.4"
|
||||
}
|
||||
}
|
||||
|
|
123
libraries/o-error/test/error-type-class.test.js
Normal file
123
libraries/o-error/test/error-type-class.test.js
Normal file
|
@ -0,0 +1,123 @@
|
|||
const errorType = require('..')
|
||||
const { expectError } = require('./support')
|
||||
|
||||
class CustomError1 extends errorType.Error {
|
||||
constructor (options) {
|
||||
super({ message: 'failed to foo', ...options })
|
||||
}
|
||||
}
|
||||
|
||||
class CustomError2 extends errorType.Error {
|
||||
constructor (options) {
|
||||
super({ message: 'failed to bar', ...options })
|
||||
}
|
||||
}
|
||||
|
||||
describe('errorType.Error', () => {
|
||||
it('handles a custom error type with a cause', () => {
|
||||
function doSomethingBadInternally () {
|
||||
throw new Error('internal error')
|
||||
}
|
||||
|
||||
function doSomethingBad () {
|
||||
try {
|
||||
doSomethingBadInternally()
|
||||
} catch (err) {
|
||||
throw new CustomError1({ info: { userId: 123 } }).withCause(err)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
doSomethingBad()
|
||||
expect.fail('should have thrown')
|
||||
} catch (e) {
|
||||
expectError(e, {
|
||||
name: 'CustomError1',
|
||||
klass: CustomError1,
|
||||
message: 'CustomError1: failed to foo: internal error',
|
||||
firstFrameRx: /doSomethingBad/
|
||||
})
|
||||
expect(errorType.getFullInfo(e)).to.deep.equal({ userId: 123 })
|
||||
const fullStack = errorType.getFullStack(e)
|
||||
expect(fullStack).to.match(
|
||||
/^CustomError1: failed to foo: internal error$/m
|
||||
)
|
||||
expect(fullStack).to.match(
|
||||
/^caused by: Error: internal error$/m
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('handles a custom error type with nested causes', () => {
|
||||
function doSomethingBadInternally () {
|
||||
throw new Error('internal error')
|
||||
}
|
||||
|
||||
function doBar () {
|
||||
try {
|
||||
doSomethingBadInternally()
|
||||
} catch (err) {
|
||||
throw new CustomError2({ info: { database: 'a' } }).withCause(err)
|
||||
}
|
||||
}
|
||||
|
||||
function doFoo () {
|
||||
try {
|
||||
doBar()
|
||||
} catch (err) {
|
||||
throw new CustomError1({ info: { userId: 123 } }).withCause(err)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
doFoo()
|
||||
expect.fail('should have thrown')
|
||||
} catch (e) {
|
||||
expectError(e, {
|
||||
name: 'CustomError1',
|
||||
klass: CustomError1,
|
||||
message: 'CustomError1: failed to foo: failed to bar: internal error',
|
||||
firstFrameRx: /doFoo/
|
||||
})
|
||||
expect(errorType.getFullInfo(e)).to.deep.equal({
|
||||
userId: 123,
|
||||
database: 'a'
|
||||
})
|
||||
const fullStack = errorType.getFullStack(e)
|
||||
expect(fullStack).to.match(
|
||||
/^CustomError1: failed to foo: failed to bar: internal error$/m
|
||||
)
|
||||
expect(fullStack).to.match(
|
||||
/^caused by: CustomError2: failed to bar: internal error$/m
|
||||
)
|
||||
expect(fullStack).to.match(
|
||||
/^caused by: Error: internal error$/m
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('errorType.ErrorWithStatusCode', () => {
|
||||
it('accepts a status code', () => {
|
||||
function findPage () {
|
||||
throw new errorType.ErrorWithStatusCode({
|
||||
message: 'page not found',
|
||||
info: { url: '/foo' },
|
||||
statusCode: 404
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
findPage()
|
||||
} catch (e) {
|
||||
expectError(e, {
|
||||
name: 'ErrorWithStatusCode',
|
||||
klass: errorType.ErrorWithStatusCode,
|
||||
message: 'ErrorWithStatusCode: page not found',
|
||||
firstFrameRx: /findPage/
|
||||
})
|
||||
expect(e.statusCode).to.equal(404)
|
||||
expect(e.info).to.deep.equal({url: '/foo'})
|
||||
}
|
||||
})
|
||||
})
|
83
libraries/o-error/test/error-type-util.test.js
Normal file
83
libraries/o-error/test/error-type-util.test.js
Normal file
|
@ -0,0 +1,83 @@
|
|||
const { getFullInfo, getFullStack, hasCauseInstanceOf } = require('..')
|
||||
|
||||
describe('errorType.getFullInfo', () => {
|
||||
it('works on a normal error', () => {
|
||||
const err = new Error('foo')
|
||||
expect(getFullInfo(err)).to.deep.equal({ })
|
||||
})
|
||||
|
||||
it('works on an error with .info', () => {
|
||||
const err = new Error('foo')
|
||||
err.info = { userId: 123 }
|
||||
expect(getFullInfo(err)).to.deep.equal({ userId: 123 })
|
||||
})
|
||||
|
||||
it('merges info from a cause chain', () => {
|
||||
const err1 = new Error('foo')
|
||||
const err2 = new Error('bar')
|
||||
err1.cause = err2
|
||||
err2.info = { userId: 123 }
|
||||
expect(getFullInfo(err1)).to.deep.equal({ userId: 123 })
|
||||
})
|
||||
|
||||
it('merges info from a cause chain with no info', () => {
|
||||
const err1 = new Error('foo')
|
||||
const err2 = new Error('bar')
|
||||
err1.cause = err2
|
||||
expect(getFullInfo(err1)).to.deep.equal({})
|
||||
})
|
||||
|
||||
it('merges info from a cause chain with duplicate keys', () => {
|
||||
const err1 = new Error('foo')
|
||||
const err2 = new Error('bar')
|
||||
err1.cause = err2
|
||||
err1.info = { userId: 123 }
|
||||
err2.info = { userId: 456 }
|
||||
expect(getFullInfo(err1)).to.deep.equal({ userId: 123 })
|
||||
})
|
||||
|
||||
it('works on an error with .info set to a string', () => {
|
||||
const err = new Error('foo')
|
||||
err.info = 'test'
|
||||
expect(getFullInfo(err)).to.deep.equal({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('errorType.getFullStack', () => {
|
||||
it('works on a normal error', () => {
|
||||
const err = new Error('foo')
|
||||
const fullStack = getFullStack(err)
|
||||
expect(fullStack).to.match(/^Error: foo$/m)
|
||||
expect(fullStack).to.match(/^\s+at /m)
|
||||
})
|
||||
|
||||
it('works on an error with a cause', () => {
|
||||
const err1 = new Error('foo')
|
||||
const err2 = new Error('bar')
|
||||
err1.cause = err2
|
||||
|
||||
const fullStack = getFullStack(err1)
|
||||
expect(fullStack).to.match(/^Error: foo$/m)
|
||||
expect(fullStack).to.match(/^\s+at /m)
|
||||
expect(fullStack).to.match(/^caused by: Error: bar$/m)
|
||||
})
|
||||
})
|
||||
|
||||
describe('errorType.hasCauseInstanceOf', () => {
|
||||
it('works on a normal error', () => {
|
||||
const err = new Error('foo')
|
||||
expect(hasCauseInstanceOf(null, Error)).to.be.false
|
||||
expect(hasCauseInstanceOf(err, Error)).to.be.true
|
||||
expect(hasCauseInstanceOf(err, RangeError)).to.be.false
|
||||
})
|
||||
|
||||
it('works on an error with a cause', () => {
|
||||
const err1 = new Error('foo')
|
||||
const err2 = new RangeError('bar')
|
||||
err1.cause = err2
|
||||
|
||||
expect(hasCauseInstanceOf(err1, Error)).to.be.true
|
||||
expect(hasCauseInstanceOf(err1, RangeError)).to.be.true
|
||||
expect(hasCauseInstanceOf(err1, TypeError)).to.be.false
|
||||
})
|
||||
})
|
|
@ -1,6 +1,7 @@
|
|||
'use strict'
|
||||
|
||||
var errorType = require('..')
|
||||
const { expectError } = require('./support')
|
||||
|
||||
describe('errorType', function () {
|
||||
it('defines a custom error type', function () {
|
||||
|
@ -14,29 +15,12 @@ describe('errorType', function () {
|
|||
doSomethingBad()
|
||||
expect.fail('should have thrown')
|
||||
} catch (e) {
|
||||
// should set the name to the error's name
|
||||
expect(e.name).to.equal('CustomError')
|
||||
|
||||
// should be an instance of the error type
|
||||
expect(e instanceof CustomError).to.be.true
|
||||
|
||||
// should be an instance of the built-in Error type
|
||||
expect(e instanceof Error).to.be.true
|
||||
|
||||
// should be recognised by util.isError
|
||||
expect(require('util').isError(e)).to.be.true
|
||||
|
||||
// should have a stack trace
|
||||
expect(e.stack).to.be.truthy
|
||||
|
||||
// toString should return the default error message formatting
|
||||
expect(e.toString()).to.equal('CustomError')
|
||||
|
||||
// stack should start with the default error message formatting
|
||||
expect(e.stack.split('\n')[0], 'CustomError')
|
||||
|
||||
// first stack frame should be the function where the error was thrown
|
||||
expect(e.stack.split('\n')[1]).to.match(/doSomethingBad/)
|
||||
expectError(e, {
|
||||
name: 'CustomError',
|
||||
klass: CustomError,
|
||||
message: 'CustomError',
|
||||
firstFrameRx: /doSomethingBad/
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -3,3 +3,29 @@
|
|||
var chai = require('chai')
|
||||
|
||||
global.expect = chai.expect
|
||||
|
||||
exports.expectError = function errorTypeExpectError (e, expected) {
|
||||
// should set the name to the error's name
|
||||
expect(e.name).to.equal(expected.name)
|
||||
|
||||
// should be an instance of the error type
|
||||
expect(e instanceof expected.klass).to.be.true
|
||||
|
||||
// should be an instance of the built-in Error type
|
||||
expect(e instanceof Error).to.be.true
|
||||
|
||||
// should be recognised by util.isError
|
||||
expect(require('util').isError(e)).to.be.true
|
||||
|
||||
// should have a stack trace
|
||||
expect(e.stack).to.be.truthy
|
||||
|
||||
// toString should return the default error message formatting
|
||||
expect(e.toString()).to.equal(expected.message)
|
||||
|
||||
// stack should start with the default error message formatting
|
||||
expect(e.stack.split('\n')[0], expected.name)
|
||||
|
||||
// first stack frame should be the function where the error was thrown
|
||||
expect(e.stack.split('\n')[1]).to.match(expected.firstFrameRx)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue