package/benchmarks/index.js000664 0000007773 3560116604 013062 0ustar00000000 000000 'use strict' /** * Micro benchmark */ const { WError, SError } = require('../index') const TypedError = require( './multidep_modules/error-7.2.1/node_modules/error/typed' ) const WrappedError = require( './multidep_modules/error-7.2.1/node_modules/error/wrapped' ) const WARMUP_LOOP = 5000 const RUN_LOOP = 250 * 1000 let mode = process.argv[2] if (!mode) { mode = 'alloc' } console.log('Running benchmarks', mode) class ServerError extends SError {} class ServerListenError extends WError {} const ServerTypedError = TypedError({ type: 'server.5xx', message: '{title} server error, status={statusCode}', title: null, statusCode: null }) const ServerListenWrappedError = WrappedError({ message: 'server: {origMessage}', type: 'server.listen-failed', requestedPort: null, host: null }) const out = { result: null } if (mode === 'alloc') { allocTypedError(WARMUP_LOOP) console.log('allocTypedError', allocTypedError(RUN_LOOP)) allocWrappedError(WARMUP_LOOP) console.log('allocWrappedError', allocWrappedError(RUN_LOOP)) allocSError(WARMUP_LOOP) console.log('allocSError', allocSError(RUN_LOOP)) allocWError(WARMUP_LOOP) console.log('allocWError', allocWError(RUN_LOOP)) } else if (mode === 'stringify') { stringifyTypedError(WARMUP_LOOP) console.log('stringifyTypedError', stringifyTypedError(RUN_LOOP)) stringifyWrappedError(WARMUP_LOOP) console.log('stringifyWrappedError', stringifyWrappedError(RUN_LOOP)) stringifySError(WARMUP_LOOP) console.log('stringifySError', stringifySError(RUN_LOOP)) stringifyWError(WARMUP_LOOP) console.log('stringifyWError', stringifyWError(RUN_LOOP)) } function allocTypedError (count) { const start = Date.now() for (let i = 0; i < count; i++) { out.result = ServerTypedError({ title: 'some title', statusCode: 500 }) } return Date.now() - start } function stringifyTypedError (count) { const start = Date.now() const err = ServerTypedError({ title: 'some title', statusCode: 500 }) Object.defineProperty(err, 'stack', { enumerable: true, configurable: true }) for (let i = 0; i < count; i++) { out.result = JSON.stringify(err) } return Date.now() - start } function allocWrappedError (count) { const start = Date.now() for (let i = 0; i < count; i++) { out.result = ServerListenWrappedError( new Error('EADDRINUSE'), { requestedPort: 3000, host: 'localhost' } ) } return Date.now() - start } function stringifyWrappedError (count) { const start = Date.now() const err = ServerListenWrappedError( new Error('EADDRINUSE'), { requestedPort: 3000, host: 'localhost' } ) Object.defineProperty(err, 'stack', { enumerable: true, configurable: true }) for (let i = 0; i < count; i++) { out.result = JSON.stringify(err) } return Date.now() - start } function allocSError (count) { const start = Date.now() for (let i = 0; i < count; i++) { out.result = ServerError.create( '{title} server error, status={statusCode}', { title: 'some title', statusCode: 500 } ) } return Date.now() - start } function stringifySError (count) { const start = Date.now() const err = ServerError.create( '{title} server error, status={statusCode}', { title: 'some title', statusCode: 500 } ) for (let i = 0; i < count; i++) { out.result = JSON.stringify(err) } return Date.now() - start } function allocWError (count) { const start = Date.now() for (let i = 0; i < count; i++) { out.result = ServerListenError.wrap( 'server', new Error('EADDRINUSE'), { title: 'some title', statusCode: 500 } ) } return Date.now() - start } function stringifyWError (count) { const start = Date.now() const err = ServerListenError.wrap( 'server', new Error('EADDRINUSE'), { title: 'some title', statusCode: 500 } ) for (let i = 0; i < count; i++) { out.result = JSON.stringify(err) } return Date.now() - start } package/index.js000664 0000025164 3560116604 010737 0ustar00000000 000000 'use strict' const assert = require('assert') /** @typedef {{ type?: string; errno?: string; syscall?: string; cause?(): Error; fullType?(this: CustomError): string; info?(): Record; toJSON?(): Record; } & Error} CustomError */ const nargs = /\{([0-9a-zA-Z_]+)\}/g const lowerCaseKebabRegex = /([a-z])([0-9A-Z])/g const upperCaseKebabRegex = /([A-Z])([A-Z])(?=[a-z])/g const PLAIN_ERROR_FIELDS = [ 'code', 'errno', 'syscall', 'status', 'statusCode', 'time', 'hostname', 'region', 'requestId', 'retryable', 'description', 'path', 'actual', 'expected', 'operator' ] const EMPTY_OBJECT = {} /** @type {Map} */ const typeNameCache = new Map() /** @type {(o: object, k: string) => unknown} */ const reflectGet = Reflect.get class StructuredError extends Error { /** * @param {string} message * @param {object} info */ constructor (message, info) { super(message) assert(typeof message === 'string') assert(info !== null && typeof info === 'object') /** @type {string} */ this.name = this.constructor.name /** @type {string} */ this.type = getTypeNameCached(this.name) /** @type {object} */ this.__info = info } /** @returns {Record} */ info () { return { ...this.__info } } /** @returns {Record} */ toJSON () { return { ...this.__info, message: this.message, stack: this.stack, type: this.type, name: this.name } } /** @returns {string} */ static get type () { return getTypeNameCached(this.name) } /** * @param {CustomError | null} error * @returns {Record} */ static getInfo (error) { if (!error) return {} if (typeof error.info !== 'function') { return { ...error } } return error.info() } /** * @param {string} messageTmpl * @param {Record} [info] * @returns {StructuredError} */ static create (messageTmpl, info) { assert(typeof messageTmpl === 'string') const msg = stringTemplate(messageTmpl, info) return new this(msg, info || EMPTY_OBJECT) } } exports.SError = StructuredError class WrappedError extends Error { /** * @param {string} message * @param {CustomError} cause * @param {object} info */ constructor (message, cause, info) { super(message) assert(typeof message === 'string') assert(info !== null && typeof info === 'object') assert(isError(cause)) /** @type {string} */ this.name = this.constructor.name /** @type {string} */ this.type = getTypeNameCached(this.name) /** @type {object} */ this.__info = info /** @type {CustomError} */ this.__cause = cause } /** @returns {string} */ fullType () { /** @type {string} */ let causeType if (typeof this.__cause.fullType === 'function') { causeType = this.__cause.fullType() } else if (this.__cause.type) { causeType = this.__cause.type } else if (this.__cause.errno || this.__cause.syscall) { causeType = 'error.wrapped-io.' + (this.__cause.syscall || 'unknown') + '.' + (this.__cause.errno || '') } else { causeType = 'error.wrapped-unknown' } return this.type + '~!~' + causeType } /** @returns {CustomError} */ cause () { return this.__cause } /** @returns {Record} */ info () { return WrappedError.fullInfo(this.cause(), this.__info) } /** @returns {Record} */ toJSON () { /** @type {Record} */ let causeJSON if (typeof this.__cause.toJSON === 'function') { causeJSON = this.__cause.toJSON() } else { causeJSON = getJSONForPlainError(this.__cause) } if (causeJSON.stack) { delete causeJSON.stack } return { ...this.info(), message: this.message, stack: this.stack, type: this.type, fullType: this.fullType(), name: this.name, cause: causeJSON } } /** @returns {string} */ static get type () { return getTypeNameCached(this.name) } /** * @param {Error} err * @returns {string} */ static fullStack (err) { return fullStack(err) } /** * @param {Error} err * @param {string} name * @returns {CustomError | null} */ static findCauseByName (err, name) { return findCauseByName(err, name) } /** * @param {CustomError | null} cause * @param {object} [info] * @returns {Record} */ static fullInfo (cause, info) { /** @type {Record | undefined} */ let existing if (cause && typeof cause.info === 'function') { existing = cause.info() } else if (cause) { existing = getInfoForPlainError(cause) } if (existing) { return { ...existing, ...info } } return { ...info } } /** * @param {string} messageTmpl * @param {Error} cause * @param {object} [info] * @returns {WrappedError} */ static wrap (messageTmpl, cause, info) { assert(typeof messageTmpl === 'string') assert(isError(cause)) let msg = stringTemplate( messageTmpl, WrappedError.fullInfo(cause, info) ) if (!info || !reflectGet(info, 'skipCauseMessage')) { msg = msg + ': ' + cause.message } return new this(msg, cause, info || EMPTY_OBJECT) } } exports.WError = WrappedError class MultiError extends Error { /** * @param {CustomError[]} errors */ constructor (errors) { assert(Array.isArray(errors)) assert(errors.length >= 1) for (const err of errors) { assert(isError(err)) } let msg = 'First of ' + String(errors.length) msg += ' error' + (errors.length > 1 ? 's' : '') msg += ': ' + errors[0].message super(msg) /** @type {CustomError[]} */ this.__errors = errors /** @type {string} */ this.name = this.constructor.name /** @type {string} */ this.type = createTypeStr(this.name) + '--' + getTypeNameCached(errors[0].name) } /** @returns {CustomError[]} */ errors () { return this.__errors.slice() } /** * @returns {{ * message: string, * stack: string, * type: string, * name: string, * errors: object[] * }} */ toJSON () { /** @type {object[]} */ const out = [] for (const e of this.__errors) { if (typeof e.toJSON === 'function') { const nestedJSON = e.toJSON() if (nestedJSON.stack) { delete nestedJSON.stack } out.push(nestedJSON) } else { out.push(getJSONForPlainError(e)) } } return { message: this.message, stack: this.stack || '', type: this.type, name: this.name, errors: out } } /** * @param {Error[]} errors * @returns {null | Error | MultiError} */ static errorFromList (errors) { assert(Array.isArray(errors)) if (errors.length === 0) { return null } if (errors.length === 1) { assert(isError(errors[0])) return errors[0] } return new this(errors) } } exports.MultiError = MultiError /** * @param {CustomError} err * @param {string} name * @returns {CustomError | null} */ function findCauseByName (err, name) { assert(isError(err)) assert(typeof name === 'string') assert(name.length > 0) /** @type {CustomError | null} */ let currentErr = err while (currentErr) { if (currentErr.name === name) { return currentErr } currentErr = typeof currentErr.cause === 'function' ? currentErr.cause() : null } return null } exports.findCauseByName = findCauseByName /** * @param {CustomError} err * @returns {string} */ function fullStack (err) { assert(isError(err)) const stack = err.stack || '' if (typeof err.cause === 'function') { return stack + '\nCaused by: ' + fullStack(err.cause()) } return stack || '' } exports.fullStack = fullStack /** * @param {CustomError | null} error * @returns {Record} */ function getInfo (error) { return StructuredError.getInfo(error) } exports.getInfo = getInfo /** * @param {string} messageTmpl * @param {Error} cause * @param {object} info * @returns {WrappedError} */ function wrapf (messageTmpl, cause, info) { return WrappedError.wrap(messageTmpl, cause, info) } exports.wrapf = wrapf /** * @param {string} messageTmpl * @param {Record} [info] * @returns {StructuredError} */ function errorf (messageTmpl, info) { return StructuredError.create(messageTmpl, info) } exports.errorf = errorf /** * @param {string} name * @returns {string} */ function getTypeNameCached (name) { let type = typeNameCache.get(name) if (type) { return type } type = createTypeStr(name) typeNameCache.set(name, type) return type } exports.getTypeName = getTypeNameCached /** * @param {string} name * @returns {string} */ function createTypeStr (name) { if (name === 'SError') { return 'structured.error' } else if (name === 'WError') { return 'wrapped.error`' } return name .replace(lowerCaseKebabRegex, '$1.$2') .replace(upperCaseKebabRegex, '$1.$2') .toLowerCase() } /** * @param {CustomError} cause * @returns {Record} */ function getInfoForPlainError (cause) { /** @type {Record} */ const info = {} for (const field of PLAIN_ERROR_FIELDS) { const v = reflectGet(cause, field) if (typeof v !== 'undefined') { info[field] = v } } return info } /** * @param {Error} err * @returns {boolean} */ function isError (err) { return Object.prototype.toString.call(err) === '[object Error]' } /** * @param {Error} err * @returns {Record} */ function getJSONForPlainError (err) { const obj = getInfoForPlainError(err) Object.assign(obj, { message: err.message, type: getTypeNameCached(err.name), name: err.name }) return obj } /** * Taken from https://www.npmjs.com/package/string-template. * source: https://github.com/Matt-Esch/string-template */ /** * @param {string} string * @param {Record} [object] * @returns {string} */ function stringTemplate (string, object) { if (!object) return string return string.replace(nargs, function replaceArg ( /** @type {string} */ match, /** @type {string} */ word, /** @type {number} */ index ) { if (string[index - 1] === '{' && string[index + match.length] === '}' ) { return word } else { const result = word in object ? object[word] : null if (result === null || result === undefined) { return '' } return String(result) } }) } package/test/index.js000664 0000000122 3560116604 011701 0ustar00000000 000000 'use strict' require('./multi.js') require('./typed.js') require('./wrapped.js') package/test/multi.js000664 0000005237 3560116604 011740 0ustar00000000 000000 'use strict' const test = require('@pre-bundled/tape') const { MultiError, errorf, WError } = require('../index.js') test('a MultiError', function t (assert) { class FanoutError extends MultiError {} const error1 = FanoutError.errorFromList([]) assert.equal(error1, null) const tempError = errorf('one error') const error2 = FanoutError.errorFromList([tempError]) assert.ok(error2) assert.equal(error2, tempError) assert.equal(error2 && error2.message, 'one error') const error3 = new MultiError([tempError]) assert.ok(error3) assert.notEqual(error3, tempError) assert.equal(error3.message, 'First of 1 error: one error') assert.deepEqual(error3.errors(), [tempError]) assert.equal(error3.name, 'MultiError') assert.equal(error3.type, 'multi.error--structured.error') assert.equal(JSON.stringify(error3), JSON.stringify({ message: 'First of 1 error: one error', stack: error3.stack, type: 'multi.error--structured.error', name: 'MultiError', errors: [{ message: 'one error', type: 'structured.error', name: 'StructuredError' }] })) class LevelReadError extends WError {} const dbErr1 = new Error('DB not open') const dbErr2 = new Error('DB already closed') const wErr1 = LevelReadError.wrap( 'could not read key: {key}', dbErr1, { key: 'foo' } ) const wErr2 = LevelReadError.wrap( 'could not read key: {key}', dbErr2, { key: 'bar' } ) const error4 = /** @type {MultiError} */ (FanoutError.errorFromList([wErr1, wErr2])) assert.ok(error4) assert.equal(error4.message, 'First of 2 errors: could not read key: foo: DB not open') assert.deepEqual(error4.errors(), [wErr1, wErr2]) assert.equal(error4.name, 'FanoutError') assert.equal(error4.type, 'fanout.error--level.read.error') assert.equal(JSON.stringify(error4), JSON.stringify({ message: 'First of 2 errors: could not read key: foo: ' + 'DB not open', stack: error4.stack, type: 'fanout.error--level.read.error', name: 'FanoutError', errors: [{ key: 'foo', message: 'could not read key: foo: DB not open', type: 'level.read.error', fullType: 'level.read.error~!~error.wrapped-unknown', name: 'LevelReadError', cause: { message: 'DB not open', type: 'error', name: 'Error' } }, { key: 'bar', message: 'could not read key: bar: DB already closed', type: 'level.read.error', fullType: 'level.read.error~!~error.wrapped-unknown', name: 'LevelReadError', cause: { message: 'DB already closed', type: 'error', name: 'Error' } }] })) assert.end() }) package/test/typed.js000664 0000004072 3560116604 011727 0ustar00000000 000000 'use strict' const test = require('@pre-bundled/tape') const { SError } = require('../index.js') test('a server error', function t (assert) { class Server5XXError extends SError {} const error = Server5XXError.create( '{title} server error, status={statusCode}', { title: 'some title', statusCode: 500 } ) assert.equal(Server5XXError.type, 'server.5xx.error') assert.equal(error.type, 'server.5xx.error') assert.deepEqual(error.info(), { statusCode: 500, title: 'some title' }) assert.equal(error.message, 'some title server error, status=500') assert.equal(error.toString(), 'Server5XXError: some title server error, status=500') assert.deepEqual(error.info(), { title: 'some title', statusCode: 500 }) assert.deepEqual(error.toJSON(), { message: error.message, name: error.name, stack: error.stack, title: 'some title', statusCode: 500, type: error.type }) assert.end() }) test('null fields', function t (assert) { class NullError extends SError {} const e = NullError.create('myError', { length: 'foo', buffer: null, state: null, expecting: null }) assert.equal(e.type, 'null.error') assert.equal(NullError.type, 'null.error') assert.end() }) test('a client error', function t (assert) { class Client4XXError extends SError {} const error2 = Client4XXError.create( '{title} client error, status={statusCode}', { title: 'some title', statusCode: 404 } ) assert.equal(error2.type, 'client.4xx.error') assert.deepEqual(error2.info(), { statusCode: 404, title: 'some title' }) assert.equal(error2.message, 'some title client error, status=404') assert.equal(error2.toString(), 'Client4XXError: some title client error, status=404') assert.deepEqual(error2.info(), { title: 'some title', statusCode: 404 }) assert.deepEqual(error2.toJSON(), { message: error2.message, name: error2.name, stack: error2.stack, title: 'some title', statusCode: 404, type: error2.type }) assert.end() }) package/test/wrapped.js000664 0000017434 3560116604 012252 0ustar00000000 000000 'use strict' const test = require('@pre-bundled/tape') const net = require('net') const { WError, getTypeName } = require('../index.js') /** @type {(o: object, k: string) => unknown} */ const reflectGet = Reflect.get test('can create a wrapped error', function t (assert) { class ServerListenFailedError extends WError {} /** @type {Error & { code?: string }} */ const err = new Error('listen EADDRINUSE') err.code = 'EADDRINUSE' const err2 = ServerListenFailedError.wrap( 'server failed', err, { requestedPort: 3426, host: 'localhost' } ) assert.equal( ServerListenFailedError.type, 'server.listen.failed.error' ) assert.equal(err2.message, 'server failed: listen EADDRINUSE') assert.deepEqual(err2.info(), { requestedPort: 3426, host: 'localhost', code: 'EADDRINUSE' }) assert.equal(err2.cause(), err) assert.equal(err2.toString(), 'ServerListenFailedError: server failed: listen EADDRINUSE') assert.equal(JSON.stringify(err2), JSON.stringify({ code: 'EADDRINUSE', requestedPort: 3426, host: 'localhost', message: 'server failed: listen EADDRINUSE', stack: err2.stack, type: 'server.listen.failed.error', fullType: 'server.listen.failed.error~!~' + 'error.wrapped-unknown', name: 'ServerListenFailedError', cause: { code: err.code, message: err.message, type: 'error', name: err.name } })) assert.end() }) test('can create wrapped error with syscall', function t (assert) { class SyscallError extends WError {} /** @type {Error & { code?: string, syscall?: string }} */ const err = new Error('listen EADDRINUSE') err.code = 'EADDRINUSE' err.syscall = 'listen' const err2 = SyscallError.wrap( 'tchannel socket error ({code} from {syscall})', err ) assert.equal(err2.message, 'tchannel socket error ' + '(EADDRINUSE from listen): listen EADDRINUSE') assert.deepEqual(err2.info(), { syscall: 'listen', code: 'EADDRINUSE' }) assert.equal(err2.type, 'syscall.error') assert.end() }) test('wrapping with skipCauseMessage', function t (assert) { class SyscallError extends WError {} /** @type {Error & { code?: string, syscall?: string }} */ const err = new Error('listen EADDRINUSE') err.code = 'EADDRINUSE' err.syscall = 'listen' const err2 = SyscallError.wrap( 'tchannel socket error ({code} from {syscall})', err, { skipCauseMessage: true } ) assert.equal(err2.message, 'tchannel socket error ' + '(EADDRINUSE from listen)') assert.deepEqual(err2.info(), { syscall: 'listen', code: 'EADDRINUSE', skipCauseMessage: true }) assert.equal(err2.type, 'syscall.error') assert.end() }) test('wrapping twice', function t (assert) { class ReadError extends WError {} class DatabaseError extends WError {} class BusinessError extends WError {} const err = BusinessError.wrap( 'business', DatabaseError.wrap( 'db', ReadError.wrap('read', new Error('oops')) ) ) assert.ok(err) assert.equal(err.message, 'business: db: read: oops') assert.equal(err.type, 'business.error') assert.equal(err.fullType(), 'business.error~!~' + 'database.error~!~' + 'read.error~!~' + 'error.wrapped-unknown') assert.equal(JSON.stringify(err), JSON.stringify({ message: 'business: db: read: oops', stack: err.stack, type: 'business.error', fullType: 'business.error~!~database.error~!~' + 'read.error~!~error.wrapped-unknown', name: 'BusinessError', cause: { message: 'db: read: oops', type: 'database.error', fullType: 'database.error~!~' + 'read.error~!~error.wrapped-unknown', name: 'DatabaseError', cause: { message: 'read: oops', type: 'read.error', fullType: 'read.error~!~error.wrapped-unknown', name: 'ReadError', cause: { message: 'oops', type: 'error', name: 'Error' } } } })) assert.end() }) test('handles bad recursive strings', function t (assert) { class ReadError extends WError {} const err2 = ReadError.wrap( 'read: {code}', new Error('hi'), { code: 'extra {code}' } ) assert.ok(err2) assert.equal(err2.message, 'read: extra {code}: hi') assert.end() }) test('can wrap real IO errors', function t (assert) { class ServerListenFailedError extends WError {} const otherServer = net.createServer() otherServer.once('listening', onPortAllocated) otherServer.listen(0) /** @returns {void} */ function onPortAllocated () { const addr = /** @type {{port: number}} */ (otherServer.address()) const port = addr.port const server = net.createServer() server.on('error', onError) server.listen(port) /** * @param {Error} cause * @returns {void} */ function onError (cause) { const err = ServerListenFailedError.wrap( 'server listen failed', cause, { host: 'localhost', requestedPort: port } ) otherServer.close() assertOnError(err, cause, port) } } /** * @param {ServerListenFailedError} err * @param {Error} cause * @param {number} port * @returns {void} */ function assertOnError (err, cause, port) { assert.ok(err.message.indexOf('server listen failed: ') >= 0) assert.ok(err.message.indexOf('listen EADDRINUSE') >= 0) assert.deepEqual(err.info(), { requestedPort: port, host: 'localhost', code: 'EADDRINUSE', syscall: 'listen', errno: 'EADDRINUSE' }) assert.equal(err.cause(), cause) assert.ok(err.toString().indexOf('ServerListenFailedError: ') >= 0) assert.ok(err.toString().indexOf('server listen failed: ') >= 0) assert.ok(err.toString().indexOf('listen EADDRINUSE') >= 0) assert.equal(JSON.stringify(err), JSON.stringify({ code: 'EADDRINUSE', errno: 'EADDRINUSE', syscall: 'listen', host: 'localhost', requestedPort: port, message: err.message, stack: err.stack, type: 'server.listen.failed.error', fullType: 'server.listen.failed.error~!~' + 'error.wrapped-io.listen.EADDRINUSE', name: 'ServerListenFailedError', cause: { code: 'EADDRINUSE', errno: 'EADDRINUSE', syscall: 'listen', message: err.cause().message, type: 'error', name: 'Error' } })) assert.end() } }) test('can wrap assert errors', function t (assert) { class TestError extends WError {} const err = TestError.wrap('error', createAssertionError()) assert.deepEqual(Reflect.get(err.cause(), 'actual'), 'a') if (err.message === "error: 'a' === 'b'") { assert.equal(err.message, "error: 'a' === 'b'") } else { assert.ok(/[eE]xpected /.test(err.message)) assert.ok(err.message.includes('strictly equal')) } assert.ok(err.cause().name.includes('AssertionError')) const operator = reflectGet(err.info(), 'operator') assert.ok(operator === '===' || operator === 'strictEqual') assert.equal(JSON.stringify(err), JSON.stringify({ code: 'ERR_ASSERTION', actual: 'a', expected: 'b', operator: operator, message: 'error: ' + err.cause().message, stack: err.stack, type: 'test.error', fullType: 'test.error~!~error.wrapped-unknown', name: 'TestError', cause: { code: 'ERR_ASSERTION', actual: 'a', expected: 'b', operator: reflectGet(err.cause(), 'operator'), message: err.cause().message, type: getTypeName(err.cause().name), name: err.cause().name } })) assert.end() }) /** @returns {Error} */ function createAssertionError () { try { require('assert').strictEqual('a', 'b') return new Error('never') } catch (_err) { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return /** @type {Error} */ (_err) } } package/package.json000664 0000002641 3560116604 011553 0ustar00000000 000000 { "name": "error", "version": "10.4.0", "description": "Custom errors", "keywords": [], "author": "Raynos ", "repository": "git://github.com/Raynos/error.git", "main": "index", "homepage": "https://github.com/Raynos/error", "contributors": [ { "name": "Raynos" } ], "bugs": { "url": "https://github.com/Raynos/error/issues", "email": "raynos2@gmail.com" }, "binDependencies": { "istanbul": "0.3.13", "tsdocstandard": "15.2.2", "type-coverage": "2.4.3", "typescript": "3.8.3" }, "tsdocstandard": { "ignore": [ "benchmarks/index.js" ] }, "dependencies": {}, "devDependencies": { "@pre-bundled/tape": "5.0.0", "@types/node": "13.13.4", "npm-bin-deps": "1.8.2" }, "licenses": [ { "type": "MIT", "url": "http://github.com/Raynos/error/raw/master/LICENSE" } ], "scripts": { "check": "npr tsc -p .", "lint": "npr tsdocstandard -v", "test": "npm run check && npm run lint && node test/index.js && npm run type-coverage", "type-coverage": "npr type-coverage --detail --strict --ignore-catch --at-least 100", "travis-test": "npr istanbul cover ./test/index.js && ((cat coverage/lcov.info | coveralls) || exit 0)", "cover": "npr istanbul cover --report none --print detail ./test/index.js", "view-cover": "npr istanbul report html && google-chrome ./coverage/index.html" } } package/tsconfig.json000664 0000001141 3560116604 011766 0ustar00000000 000000 { "compilerOptions": { "types": ["node"], "target": "es2018", "lib": ["es2018"], "noEmit": true, "module": "commonjs", "allowJs": true, "checkJs": true, "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, "noUnusedLocals": true, "noUnusedParameters": true, "strict": true, "baseUrl": "./", "paths": { // "@pre-bundled/rimraf": ["./types/pre-bundled__rimraf"], "@pre-bundled/tape": ["./types/pre-bundled__tape"], "*" : ["./types/*"] } }, "include": [ "types/**/*.d.ts", "index.js", "test/**/*.js" ] } package/types/pre-bundled__tape/tsconfig.json000664 0000000507 3560116604 016510 0ustar00000000 000000 { "compilerOptions": { "module": "commonjs", "noEmit": true, "forceConsistentCasingInFileNames": true, "baseUrl": "../", "typeRoots": [ "../" ], "types": [], "lib": ["es6"], "noImplicitAny": true, "noImplicitThis": true, "strictNullChecks": true, "strictFunctionTypes": true } } package/types/pre-bundled__tape/tslint.json000664 0000000175 3560116604 016212 0ustar00000000 000000 { "extends": [ "dtslint/dtslint.json", "../../tslint.json" ], "rules": { "unified-signatures": false } } package/MIGRATION.md000664 0000002052 3560116604 011134 0ustar00000000 000000 ## Migration ## Version 10 The 10th version is a complete rewrite. Check out the `v7.x` branch for the older implementation. ## Version 7 The `message` parameter to `TypedError` is now required. Previously `message` was optional for `TypedError`. ## Version 6 The `WrappedError` class now exposes the error that is being wrapped as a `cause` field instead of an `original` field. The following properties have been reserver on the wrapped error class: `cause`, `fullType`, `causeMessage` ## Version 5 There were no breaking changes... ## Version 4 The `TypedError` function now has mandatory arguments. The `type` and `message` arguments for `TypedError` are required. ## Version 3 The `TypedError` class now uses `string-template` for message formatting. Previously: ```js var FooError = TypedError({ type: 'foo.x' message: 'Got an error %s' }); FooError('Oops'); ``` Currently: ```js var FooError = TypedError({ type: 'foo.x', message: 'Got an error {ctx}', ctx: null }); FooError({ ctx: 'Oops' }); ``` ## Version 2 Original version package/README.md000664 0000026125 3560116604 010547 0ustar00000000 000000 # error Wrap errors with more context. ## Inspiration This module is inspired by the go error libraries that have simple functions for creating & wrapping errors. This is based on libraries like [eris][eris] & [pkg/errors][pkg-errors] ## Older version of `error` If you are looking for the older v7 version of error you should check [v7.x][7.x] branch ## Using `error` with `async` / `await` Check out [`resultify`](https://www.npmjs.com/package/resultify) ! The rest of the examples use plain vanilla callbacks. ## Motivation Wrapping errors when bubbling up instead of just doing `if (err) return cb(err)` allows you to pass more context up the stack. Common example include passing along parameters from the DB read related to the failure or passing along any context from the user in a HTTP request when doing a failure. This can give you nice to read messages that include more information about the failure as it bubbles up. There is more information about how to handle errors in this article [Don't just check errors, handle them gracefully][dave] If you want a deep dive into the difference between [Programming and Operational errors](https://www.joyent.com/node-js/production/design/errors) please check out [this guide](https://www.joyent.com/node-js/production/design/errors) examples: ```js const { wrapf } = require('error') function authenticatRequest(req) { authenticate(req.user, (err) => { if (err) { return cb(wrapf('authenticate failed', err)) } cb(null) }) } ``` or ```js const { wrapf } = require('error') function readFile(path, cb) { fs.open(path, 'r', (err, fd) => { if (err) { return cb(wrapf('open failed', err, { path })) } const buf = Buffer.alloc(64 * 1024) fs.read(fd, buf, 0, buf.length, 0, (err) => { if (err) { return cb(wrapf('read failed', err, { path })) } fs.close(fd, (err) => { if (err) { return cb(wrapf('close failed', err, { path })) } cb(null, buf) }) }) }) } ``` ## Structured errors ```js const { SError } = require('error') class ServerError extends SError {} class ClientError extends SError {} const err = ServerError.create( '{title} server error, status={statusCode}', { title: 'some title', statusCode: 500 } ) const err2 = ClientError.create( '{title} client error, status={statusCode}', { title: 'some title', statusCode: 404 } ) ``` ## Wrapped Errors ```js const net = require('net'); const { WError } = require('error') class ServerListenError extends WError {} var server = net.createServer(); server.on('error', function onError(err) { if (err.code === 'EADDRINUSE') { throw ServerListenFailedError.wrap( 'error in server, on port={requestPort}', err, { requestPort: 3000, host: null } ) } else { throw err; } }); server.listen(3000); ``` ## Comparison to Alternatives. There are alternative existing libraries for creating typed and wrapped errors on npm. Here's a quick comparison to some alternatives. ### [`verror`][verror] This module takes inspiration from `verror` and adds improvements. - You can pass extra fields as meta data on the error - The templating forces dynamic strings to be extra fields. - Uses ES6 classes for inheritance. This gives your errors unique class names and makes them show up in heapdumps. - Has JSON.stringify support ### [`error@7.x`][7.x] This package used to have a completely different API on the [7.x][7.x] branch. - New `error` module uses actual classes instead of dynamically monkey patching fields onto `new Error()` - Implementation is more static, previous code was very dynamic - Simpler API, see the message & properties in one place. - `wrapf` & `errorf` helpers for less boilerplate. ### Hand writing `Error` sub classes. You can create your own Error classes by hand. This tends to lead to 10-20 lines of boilerplate per error which is replace with one line by using the `error` module; aka ```js class AccountsServerFailureError extends SError {} class ConnectionResetError extends WError {} ``` ### [`ono`][ono] The `ono` package has similar functionality with a different API - `ono` encourages plain errors instead of custom errors by default - `error` has zero dependencies - `error` is only one simple file. `ono` is 10. - `error` implementation is more static, ono is very dynamic. ## Documentation This package implements three classes, `WError`; `SError` & `MultiError` You are expected to subclass either `WError` or `SError`; - `SError` stands for `Structured Error`; it's an error base class for adding informational fields to your error beyond just having a message. - `WError` stands for `Wrapped Error`; it's an error base class for when you are wrapping an existing error with more information. The `MultiError` class exists to store an array of errors but still return a single `Error`; This is useful if your doing a parallel operation and you want to wait for them all to finish and do something with all of the failures. Some utility functions are also exported: - `findCauseByName`; See if error or any of it's causes is of the type name. - `fullStack`; Take a wrapped error and compute a full stack. - `wrapf`; Utility function to quickly wrap - `errorf`; Utility function to quickly create an error - `getInfo`; Utility function to get the info for any error object. Calls `err.info()` if the method exists. ### `WError` Example: ```js class ServerListenError extends WError {} ServerListenError.wrap('error in server', err, { port: 3000 }) ``` When using the `WError` class it's recommended to always call the static `wrap()` method instead of calling the constructor directly. Example (without cause message): ```js class ApplicationStartupError extends WError {} ApplicationStartupError.wrap( 'Could not start the application cleanly: {reason}', err, { skipCauseMessage: true, reason: 'Failed to read from disk' } ) ``` Setting `skipCauseMessage: true` will not append the cause error message but still make the cause object available. ### `const werr = new WError(message, cause, info)` Internal constructor, should pass a `message` string, a `cause` error and a `info` object (or `null`). ### `WError.wrap(msgTmpl, cause, info)` `wrap()` method to create error instances. This applies the [`string-template`][string-template] templating to `msgTmpl` with `info` as a parameter. The `cause` parameter must be an `error` The `info` parameter is an object or `null`. The `info` parameter can contain the field `skipCauseMessage: true` which will make `WError` not append `: ${causeMessage}` to the message of the error. ### `werr.type` The `type` field is the machine readable type for this error. Always use `err.type` and never `err.message` when trying to determine what kind of error it is. The `type` field is unlikely to change but the `message` field can change. ### `werr.fullType()` Calling `fullType` will compute a full type for this error and any causes that it wraps. This gives you a long `type` string that's a concat for every wrapped cause. ### `werr.cause()` Returns the `cause` error. ### `werr.info()` Returns the `info` object passed on. This is merged with the info of all `cause` errors up the chain. ### `werr.toJSON()` The `WError` class implements `toJSON()` so that the JSON serialization makes sense. ### `WError.fullStack(err)` This returns a full stack; which is a concatenation of this stack trace and the stack trace of all causes in the cause chain ### `WError.findCauseByName(err, name)` Given an err and a name will find if the err or any causes implement the type of that name. This allows you to check if a wrapped `ApplicationError` has for example a `LevelReadError` or `LevelWriteError` in it's cause chain and handle database errors differently from all other app errors. ### `SError` Example: ```js class LevelReadError extends SError {} LevelReadError.create('Could not read key: {key}', { key: '/some/key' }) ``` When using the `SError` class it's recommended to always call the static `create()` method instead of calling the constructor directly. ### `const serr = new SError(message, info)` Internal constructor that takes a message string & an info object. ### `SError.create(messageTmpl, info)` The main way to create error objects, takes a message template and an info object. It will use [string-template][string-template] to apply the template with the `info` object as a parameter. ### `SError.getInfo(error)` Static method to `getInfo` on a maybe error. The `error` can be `null` or `undefined`, it can be a plain `new Error()` or it can be a structured or wrapped error. Will return `err.info()` if it exists, returns `{}` if its `null` and returns `{ ...err }` if its a plain vanilla error. ### `serr.type` Returns the type field. The `err.type` field is machine readable. Always use `err.type` & not `err.message` when trying to compare errors or do any introspection. The `type` field is unlikely to change but the `message` field can change. ### `serr.info()` Returns the info object for this error. ### `serr.toJSON()` This class can JSON serialize cleanly. ### `MultiError` Example: ```js class FanoutError extends MultiError {} function doStuff (filePath, cb) { fanoutDiskReads(filePath, (errors, fileContents) => { if (errors && errors.length > 0) { const err = FanoutError.errorFromList(errors) return cb(err) } // do stuff with files. }) } ``` When using the `MultiError` class it's recommended to always call the static `errorFromList` method instead of calling the constructor directly. ## Usage from typescript The `error` library does not have an `index.d.ts` but does have full `jsdoc` annotations so it should be typesafe to use. You will need to configure your `tsconfig` appropiately ... ```json { "compilerOptions": { ... "allowJs": true, ... }, "include": [ "src/**/*.js", "node_modules/error/index.js" ], "exclude": [ "node_modules" ] } ``` Typescript does not understand well type source code in `node_modules` without an `index.d.ts` by default, so you need to tell it to include the implementation of `error/index.js` during type checking and to `allowJs` to enable typechecking js + jsdoc comments. ## Installation `npm install error` ## Contributors - Raynos ## MIT Licenced [eris]: https://github.com/rotisserie/eris/tree/v0.1.0 [pkg-errors]: https://github.com/pkg/errors [7.x]: https://github.com/Raynos/error/tree/v7.x [dave]: https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully [string-template]: https://github.com/Matt-Esch/string-template [verror]: https://github.com/joyent/node-verror [ono]: https://github.com/JS-DevTools/ono package/types/pre-bundled__tape/index.d.ts000664 0000021166 3560116604 015706 0ustar00000000 000000 // TypeScript Version: 3.0 /// export = tape; /** * Create a new test with an optional name string and optional opts object. * cb(t) fires with the new test object t once all preceeding tests have finished. * Tests execute serially. */ declare function tape(name: string | tape.TestOptions, cb: tape.TestCase): void; declare function tape(name: string, opts: tape.TestOptions, cb: tape.TestCase): void; declare function tape(cb: tape.TestCase): void; declare namespace tape { interface TestCase { (test: Test): void; } /** * Available opts options for the tape function. */ interface TestOptions { skip?: boolean; // See tape.skip. timeout?: number; // Set a timeout for the test, after which it will fail. See tape.timeoutAfter. } /** * Options for the createStream function. */ interface StreamOptions { objectMode?: boolean; } /** * Generate a new test that will be skipped over. */ function skip(name: string | TestOptions, cb: TestCase): void; function skip(name: string): void; function skip(name: string, opts: TestOptions, cb: TestCase): void; function skip(cb: TestCase): void; /** * The onFinish hook will get invoked when ALL tape tests have finished right before tape is about to print the test summary. */ function onFinish(cb: () => void): void; /** * Like test(name?, opts?, cb) except if you use .only this is the only test case that will run for the entire process, all other test cases using tape will be ignored. */ function only(name: string | TestOptions, cb: TestCase): void; function only(name: string, opts: TestOptions, cb: TestCase): void; function only(cb: TestCase): void; /** * Create a new test harness instance, which is a function like test(), but with a new pending stack and test state. */ function createHarness(): typeof tape; /** * Create a stream of output, bypassing the default output stream that writes messages to console.log(). * By default stream will be a text stream of TAP output, but you can get an object stream instead by setting opts.objectMode to true. */ function createStream(opts?: StreamOptions): NodeJS.ReadableStream; interface Test { /** * Create a subtest with a new test handle st from cb(st) inside the current test. * cb(st) will only fire when t finishes. * Additional tests queued up after t will not be run until all subtests finish. */ test(name: string, cb: TestCase): void; test(name: string, opts: TestOptions, cb: TestCase): void; /** * Declare that n assertions should be run. end() will be called automatically after the nth assertion. * If there are any more assertions after the nth, or after end() is called, they will generate errors. */ plan(n: number): void; /** * Declare the end of a test explicitly. * If err is passed in t.end will assert that it is falsey. */ end(err?: unknown): void; /** * Generate a failing assertion with a message msg. */ fail(msg?: string): void; /** * Generate a passing assertion with a message msg. */ pass(msg?: string): void; /** * Automatically timeout the test after X ms. */ timeoutAfter(ms: number): void; /** * Generate an assertion that will be skipped over. */ skip(msg?: string): void; /** * Assert that value is truthy with an optional description message msg. */ ok(value: unknown, msg?: string): void; true(value: unknown, msg?: string): void; assert(value: unknown, msg?: string): void; /** * Assert that value is falsy with an optional description message msg. */ notOk(value: unknown, msg?: string): void; false(value: unknown, msg?: string): void; notok(value: unknown, msg?: string): void; /** * Assert that err is falsy. * If err is non-falsy, use its err.message as the description message. */ error(err: unknown, msg?: string): void; ifError(err: unknown, msg?: string): void; ifErr(err: unknown, msg?: string): void; iferror(err: unknown, msg?: string): void; /** * Assert that a === b with an optional description msg. */ equal(actual: T, expected: T, msg?: string): void; equals(actual: T, expected: T, msg?: string): void; isEqual(actual: T, expected: T, msg?: string): void; is(actual: T, expected: T, msg?: string): void; strictEqual(actual: T, expected: T, msg?: string): void; strictEquals(actual: T, expected: T, msg?: string): void; /** * Assert that a !== b with an optional description msg. */ notEqual(actual: unknown, expected: unknown, msg?: string): void; notEquals(actual: unknown, expected: unknown, msg?: string): void; notStrictEqual(actual: unknown, expected: unknown, msg?: string): void; notStrictEquals(actual: unknown, expected: unknown, msg?: string): void; isNotEqual(actual: unknown, expected: unknown, msg?: string): void; isNot(actual: unknown, expected: unknown, msg?: string): void; not(actual: unknown, expected: unknown, msg?: string): void; doesNotEqual(actual: unknown, expected: unknown, msg?: string): void; isInequal(actual: unknown, expected: unknown, msg?: string): void; /** * Assert that a and b have the same structure and nested values using node's deepEqual() algorithm with strict comparisons (===) on leaf nodes and an optional description msg. */ deepEqual(actual: T, expected: T, msg?: string): void; deepEquals(actual: T, expected: T, msg?: string): void; isEquivalent(actual: T, expected: T, msg?: string): void; same(actual: T, expected: T, msg?: string): void; /** * Assert that a and b do not have the same structure and nested values using node's deepEqual() algorithm with strict comparisons (===) on leaf nodes and an optional description msg. */ notDeepEqual(actual: unknown, expected: unknown, msg?: string): void; notEquivalent(actual: unknown, expected: unknown, msg?: string): void; notDeeply(actual: unknown, expected: unknown, msg?: string): void; notSame(actual: unknown, expected: unknown, msg?: string): void; isNotDeepEqual(actual: unknown, expected: unknown, msg?: string): void; isNotDeeply(actual: unknown, expected: unknown, msg?: string): void; isNotEquivalent(actual: unknown, expected: unknown, msg?: string): void; isInequivalent(actual: unknown, expected: unknown, msg?: string): void; /** * Assert that a and b have the same structure and nested values using node's deepEqual() algorithm with loose comparisons (==) on leaf nodes and an optional description msg. */ deepLooseEqual(actual: unknown, expected: unknown, msg?: string): void; looseEqual(actual: unknown, expected: unknown, msg?: string): void; looseEquals(actual: unknown, expected: unknown, msg?: string): void; /** * Assert that a and b do not have the same structure and nested values using node's deepEqual() algorithm with loose comparisons (==) on leaf nodes and an optional description msg. */ notDeepLooseEqual(actual: unknown, expected: unknown, msg?: string): void; notLooseEqual(actual: unknown, expected: unknown, msg?: string): void; notLooseEquals(actual: unknown, expected: unknown, msg?: string): void; /** * Assert that the function call fn() throws an exception. * expected, if present, must be a RegExp or Function, which is used to test the exception object. */ throws(fn: () => void, msg?: string): void; throws(fn: () => void, exceptionExpected: RegExp | typeof Error, msg?: string): void; /** * Assert that the function call fn() does not throw an exception. */ doesNotThrow(fn: () => void, msg?: string): void; doesNotThrow(fn: () => void, exceptionExpected: RegExp | typeof Error, msg?: string): void; /** * Print a message without breaking the tap output. * (Useful when using e.g. tap-colorize where output is buffered & console.log will print in incorrect order vis-a-vis tap output.) */ comment(msg: string): void; } } package/.istanbul.yml000664 0000000465 3560116604 011711 0ustar00000000 000000 instrumentation: default-excludes: false include-all-sources: true excludes: - '**/test/**' - '**/coverage/**' - '**/example/**' - '**/test.js' - '**/node_modules/istanbul/**' - '**/node_modules/tape/**' - '**/node_modules/uber-standard/**'