From cbb738e3b6e65313e955c8ab13f51a60f9ea574e Mon Sep 17 00:00:00 2001 From: Nicolas Morel Date: Wed, 23 Oct 2024 16:24:15 +0200 Subject: [PATCH 01/22] chore: pin eslint-plugin --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2170427..23c8ac9 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ }, "devDependencies": { "@hapi/code": "^9.0.0", - "@hapi/eslint-plugin": "*", + "@hapi/eslint-plugin": "^6.0.0", "@hapi/lab": "^25.1.0" }, "scripts": { From 2690ce5f579492a5ccff3ddd809b4aee5579f76f Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Thu, 30 Nov 2023 17:23:14 +0100 Subject: [PATCH 02/22] Support AbortSignal --- API.md | 14 +++- lib/index.js | 18 ++++- test/index.js | 193 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 223 insertions(+), 2 deletions(-) diff --git a/API.md b/API.md index d1cc06c..f5c08a1 100644 --- a/API.md +++ b/API.md @@ -79,9 +79,11 @@ async function register(address, name) { Throws the error passed if it matches any of the specified rules where: - `err` - the error. - `type` - a single item or an array of items of: - - An error constructor (e.g. `SyntaxError`). + - An error constructor (e.g. `SyntaxError`) - matches error created from constructor or any subclass. - `'system'` - matches any languange native error or node assertions. - `'boom'` - matches [**boom**](https://github.com/hapijs/boom) errors. + - `'abort'` - matches an `AbortError`, as generated on an `AbortSignal` by `AbortController.abort()`. + - `'timeout'` - matches an `TimeoutError`, as generated by `AbortSignal.timeout(delay)`. - an object where each property is compared with the error and must match the error property value. All the properties in the object must match the error but do not need to include all the error properties. @@ -90,6 +92,7 @@ Throws the error passed if it matches any of the specified rules where: - `override` - an error used to override `err` when `err` matches. If used with `decorate`, the `override` object is modified. - `return` - if `true`, the error is returned instead of thrown. Defaults to `false`. + - `signal` - an `AbortSignal`. Throws `signal.reason` if it has already been aborted. ### `ignore(err, types, [options])` @@ -124,3 +127,12 @@ Return `true` when `err` is one of: - `TypeError` - `URIError` - Node's `AssertionError` +- Hoek's `AssertError` + +### `isAbort(err)` + +Returns `true` when `err` is an `AbortError`, as generated on an `AbortSignal` by `AbortController.abort()`. + +### `isTimeout(err)` + +Returns `true` when `err` is an `TimeoutError`, as generated by `AbortSignal.timeout(delay)`. diff --git a/lib/index.js b/lib/index.js index 7854ba1..ce1d84f 100755 --- a/lib/index.js +++ b/lib/index.js @@ -43,6 +43,8 @@ exports.ignore = function (err, types, options = {}) { internals.catch = function (err, types, options, match) { + options.signal?.throwIfAborted(); + if (internals.match(err, types) !== match) { return; } @@ -115,9 +117,23 @@ exports.isSystem = function (err) { }; +exports.isAbort = function (err) { + + return err instanceof Error && err.name === 'AbortError'; +}; + + +exports.isTimeout = function (err) { + + return err instanceof Error && err.name === 'TimeoutError'; +}; + + internals.rules = { system: exports.isSystem, - boom: exports.isBoom + boom: exports.isBoom, + abort: exports.isAbort, + timeout: exports.isTimeout }; diff --git a/test/index.js b/test/index.js index 3f28ea4..6850509 100755 --- a/test/index.js +++ b/test/index.js @@ -107,6 +107,52 @@ describe('Bounce', () => { expect(error3).to.be.an.error('Something', SyntaxError); }); + it('rethrows only abort errors', () => { + + try { + Bounce.rethrow(new Error('Something'), 'abort'); + } + catch (err) { + var error1 = err; + } + + expect(error1).to.not.exist(); + + try { + Bounce.rethrow(AbortSignal.abort().reason, 'abort'); + } + catch (err) { + var error2 = err; + } + + expect(error2).to.be.an.error(DOMException); + expect(error2.name).to.equal('AbortError'); + }); + + it('rethrows only timeout errors', async () => { + + try { + Bounce.rethrow(new Error('Something'), 'timeout'); + } + catch (err) { + var error1 = err; + } + + expect(error1).to.not.exist(); + + try { + const signal = AbortSignal.timeout(0); + await Hoek.wait(1); + Bounce.rethrow(signal.reason, 'timeout'); + } + catch (err) { + var error2 = err; + } + + expect(error2).to.be.an.error(DOMException); + expect(error2.name).to.equal('TimeoutError'); + }); + it('rethrows only specified errors', () => { try { @@ -240,6 +286,38 @@ describe('Bounce', () => { expect(error).to.shallow.equal(orig); expect(error).to.be.an.error('Something'); }); + + it('rethrows already aborted signal reason', () => { + + const orig = new Error('Something'); + const signal = AbortSignal.abort(new Error('Fail')); + + try { + Bounce.rethrow(orig, null, { signal }); + } + catch (err) { + var error = err; + } + + expect(error).to.shallow.equal(signal.reason); + expect(error).to.be.an.error('Fail'); + }); + + it('ignores non-aborted signal', () => { + + const orig = new Error('Something'); + const signal = new AbortController().signal; + + try { + Bounce.rethrow(orig, null, { signal }); + } + catch (err) { + var error = err; + } + + expect(error).to.shallow.equal(orig); + expect(error).to.be.an.error('Something'); + }); }); describe('ignore()', () => { @@ -315,6 +393,38 @@ describe('Bounce', () => { expect(error3).to.not.exist(); }); + + it('rethrows already aborted signal reason', () => { + + const orig = new Error('Something'); + const signal = AbortSignal.abort(new Error('Fail')); + + try { + Bounce.ignore(orig, 'system', { signal }); + } + catch (err) { + var error = err; + } + + expect(error).to.shallow.equal(signal.reason); + expect(error).to.be.an.error('Fail'); + }); + + it('ignores non-aborted signal', () => { + + const orig = new Error('Something'); + const signal = new AbortController().signal; + + try { + Bounce.ignore(orig, 'system', { signal }); + } + catch (err) { + var error = err; + } + + expect(error).to.shallow.equal(orig); + expect(error).to.be.an.error('Something'); + }); }); describe('background()', () => { @@ -537,4 +647,87 @@ describe('Bounce', () => { expect(Bounce.isSystem(Boom.boomify(new TypeError()))).to.be.false(); }); }); + + describe('isAbort()', () => { + + it('identifies AbortSignal.abort() reason as abort', () => { + + expect(Bounce.isAbort(AbortSignal.abort().reason)).to.be.true(); + }); + + it('identifies DOMException AbortError as abort', () => { + + expect(Bounce.isAbort(new DOMException('aborted', 'AbortError'))).to.be.true(); + }); + + it('identifies Error with name "AbortError" as abort', () => { + + class MyAbort extends Error { + name = 'AbortError'; + } + + expect(Bounce.isAbort(new MyAbort())).to.be.true(); + }); + + it('identifies object as non-abort', () => { + + expect(Bounce.isAbort({})).to.be.false(); + }); + + it('identifies error as non-abort', () => { + + expect(Bounce.isAbort(new Error('failed'))).to.be.false(); + }); + + it('identifies object with name "AbortError" as non-abort', () => { + + expect(Bounce.isAbort({ name: 'AbortError' })).to.be.false(); + }); + + it('identifies AbortSignal.timeout() reason non-abort', async () => { + + const signal = AbortSignal.timeout(0); + await Hoek.wait(1); + expect(Bounce.isAbort(signal.reason)).to.be.false(); + }); + }); + + describe('isTimeout()', () => { + + it('identifies AbortSignal.timeout() reason as timeout', async () => { + + const signal = AbortSignal.timeout(0); + await Hoek.wait(1); + expect(Bounce.isTimeout(signal.reason)).to.be.true(); + }); + + it('identifies DOMException TimeoutError as timeout', () => { + + expect(Bounce.isTimeout(new DOMException('timed out', 'TimeoutError'))).to.be.true(); + }); + + it('identifies Error with name "TimeoutError" as timeout', () => { + + class MyTimeout extends Error { + name = 'TimeoutError'; + } + + expect(Bounce.isTimeout(new MyTimeout())).to.be.true(); + }); + + it('identifies object as non-timeout', () => { + + expect(Bounce.isTimeout({})).to.be.false(); + }); + + it('identifies error as non-timeout', () => { + + expect(Bounce.isTimeout(new Error('failed'))).to.be.false(); + }); + + it('identifies object with name "TimeoutError" as non-timeout', () => { + + expect(Bounce.isTimeout({ name: 'TimeoutError' })).to.be.false(); + }); + }); }); From 3d130e77078ba71fd0a742170e632a5b6443b103 Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Thu, 30 Nov 2023 18:27:39 +0100 Subject: [PATCH 03/22] Make signal rethrow more web-compatible --- lib/index.js | 4 +++- test/index.js | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index ce1d84f..f68b076 100755 --- a/lib/index.js +++ b/lib/index.js @@ -43,7 +43,9 @@ exports.ignore = function (err, types, options = {}) { internals.catch = function (err, types, options, match) { - options.signal?.throwIfAborted(); + if (options.signal?.aborted) { + throw options.signal.reason ?? new DOMException('This operation was aborted', 'AbortError'); + } if (internals.match(err, types) !== match) { return; diff --git a/test/index.js b/test/index.js index 6850509..7c5fa34 100755 --- a/test/index.js +++ b/test/index.js @@ -303,6 +303,23 @@ describe('Bounce', () => { expect(error).to.be.an.error('Fail'); }); + it('rethrows already aborted signal with no reason', () => { + + const orig = new Error('Something'); + const signal = AbortSignal.abort(); + + Object.defineProperty(signal, 'reason', { value: undefined }); // Simulate older API without the reason property + + try { + Bounce.rethrow(orig, null, { signal }); + } + catch (err) { + var error = err; + } + + expect(error).to.be.an.error(DOMException, 'This operation was aborted'); + }); + it('ignores non-aborted signal', () => { const orig = new Error('Something'); From 189747a3879ec83faa62f185146ad60a4789da26 Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Wed, 23 Oct 2024 16:14:02 +0200 Subject: [PATCH 04/22] Cleanup docs --- API.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/API.md b/API.md index f5c08a1..41e421c 100644 --- a/API.md +++ b/API.md @@ -83,7 +83,7 @@ Throws the error passed if it matches any of the specified rules where: - `'system'` - matches any languange native error or node assertions. - `'boom'` - matches [**boom**](https://github.com/hapijs/boom) errors. - `'abort'` - matches an `AbortError`, as generated on an `AbortSignal` by `AbortController.abort()`. - - `'timeout'` - matches an `TimeoutError`, as generated by `AbortSignal.timeout(delay)`. + - `'timeout'` - matches a `TimeoutError`, as generated by `AbortSignal.timeout(delay)`. - an object where each property is compared with the error and must match the error property value. All the properties in the object must match the error but do not need to include all the error properties. @@ -131,8 +131,14 @@ Return `true` when `err` is one of: ### `isAbort(err)` -Returns `true` when `err` is an `AbortError`, as generated on an `AbortSignal` by `AbortController.abort()`. +Returns `true` when `err` is an `AbortError`, as generated by `AbortSignal.abort()`. + +Note that unlike other errors, `AbortError` cannot be considered a class in itself. +The best way to create a custom `AbortError` is with `new DOMException(message, 'AbortError')`. ### `isTimeout(err)` -Returns `true` when `err` is an `TimeoutError`, as generated by `AbortSignal.timeout(delay)`. +Returns `true` when `err` is a `TimeoutError`, as generated by `AbortSignal.timeout(delay)`. + +Note that unlike other errors, `TimeoutError` cannot be considered a class in itself. +The best way to create a custom `TimeoutError` is with `new DOMException(message, 'TimeoutError')`. From 47d9144281a22c8681a7bee3bf60ab28eac308c9 Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Wed, 23 Oct 2024 16:11:57 +0200 Subject: [PATCH 05/22] Add typescript typings --- lib/index.d.ts | 131 +++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 7 ++- test/index.ts | 116 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 lib/index.d.ts create mode 100644 test/index.ts diff --git a/lib/index.d.ts b/lib/index.d.ts new file mode 100644 index 0000000..2ac152b --- /dev/null +++ b/lib/index.d.ts @@ -0,0 +1,131 @@ +import type { Boom } from "@hapi/boom"; + +/** + * Possible type matching rule. One of: + * - An error constructor (e.g. `SyntaxError`). + * - `'system'` - matches any language native error or node assertions. + * - `'boom'` - matches [**boom**](https://github.com/hapijs/boom) errors. + * - `'abort'` - matches an `AbortError`, as generated on an `AbortSignal` by `AbortController.abort()`. + * - `'timeout'` - matches a `TimeoutError`, as generated by `AbortSignal.timeout(delay)`. + * - an object where each property is compared with the error and must match the error property + * value. All the properties in the object must match the error but do not need to include all + * the error properties. + */ +type TypeRule = 'system' | 'boom' | 'abort' | 'timeout' | ErrorConstructor | { [key: PropertyKey]: any }; + +type Decoration = { [key: string]: any }; + +interface BounceOptions { + + /** + * An object which is assigned to the `err`, copying the properties onto the error. + */ + decorate?: { [key: string]: any }; + + /** + * An error used to override `err` when `err` matches. + * If used with `decorate`, the `override` object is modified. + */ + override?: Error; + + /** + * If `true`, the error is returned instead of thrown. Defaults to `false`. + */ + return?: boolean; +} + +/** + * Throws the error passed if it matches any of the specified rules. + * + * @param err - the error object to test. + * @param type - a single {@link TypeRule `TypeRule`} or an array of {@link TypeRule `TypeRule`}. + * @param options - optional {@link BounceOptions settings}. + * + * @returns possibly an `Error` depending on value of the `return` and `decorate` options. + */ +export function rethrow(err: any, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, decorate: D, override: E }): (E & D) | undefined; +export function rethrow(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, decorate: D }): (T & D) | undefined; +export function rethrow(err: any, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, override: E }): E | undefined; +export function rethrow(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true }): T | undefined; +export function rethrow(err: any, types: TypeRule | TypeRule[], options?: BounceOptions): void; + +/** + * The opposite action of {@link rethrow `rethrow()`}. Ignores any errors matching the specified `types`. + * Any error not matching is thrown after applying the `options`. + * + * @param err - the error object to test. + * @param type - a single {@link TypeRule `TypeRule`} or an array of {@link TypeRule `TypeRule`}. + * @param options - optional {@link BounceOptions settings}. + * + * @returns possibly an `Error` depending on value of the `return` and `decorate` options. + */ +export function ignore(err: any, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, decorate: D, override: E }): (E & D) | undefined; +export function ignore(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, decorate: D }): (T & D) | undefined; +export function ignore(err: any, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, override: E }): E | undefined; +export function ignore(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true }): T | undefined; +export function ignore(err: any, types: TypeRule | TypeRule[], options?: BounceOptions): void; + +type Action = 'rethrow' | 'ignore'; + +/** + * Awaits for the value to resolve in the background and then apply either the {@link rethrow `rethrow()`} or {@link rethrow `ignore()`} action. + * + * @param operation - a function, promise, or value that is `await`ed on inside a `try...catch` + * and any error thrown processed by the `action` rule. + * @param action - one of `'rethrow'` or`'ignore'`. Defaults to`'rethrow'`. + * @param types - same as the `types` argument passed to {@link rethrow `rethrow()`} or {@link rethrow `ignore()`}. Defaults to `'system'`. + * @param options - same as the {@link BounceOptions `options`} argument passed to {@link rethrow `rethrow()`} or {@link rethrow `ignore()`}. + */ +export function background(operation: any, action?: Action, types?: TypeRule | TypeRule[], options?: BounceOptions & { return: true, override: E, decorate: D }): Promise<(E & D) | undefined>; +export function background(operation: any, action?: Action, types?: TypeRule | TypeRule[], options?: BounceOptions & { return: true, override: E }): Promise; +export function background(operation: any, action?: Action, types?: TypeRule | TypeRule[], options?: BounceOptions & { return: true }): Promise; +export function background(operation: any, action?: Action, types?: TypeRule | TypeRule[], options?: BounceOptions): Promise; + +/** + * Returns `true` when `err` is a [**boom**](https://github.com/hapijs/boom) error. + * + * @param err - Object to test. + */ +export function isBoom(err: unknown): err is Boom; + +/** + * Returns `true` when `err` is an error. + * + * @param err - Object to test. + */ +export function isError(err: unknown): err is Error; + +/** + * Return `true` when `err` is one of: + * - `EvalError` + * - `RangeError` + * - `ReferenceError` + * - `SyntaxError` + * - `TypeError` + * - `URIError` + * - Node's `AssertionError` + * - Hoek's `AssertError` + * + * @param err - Object to test. + */ +export function isSystem(err: unknown): err is Error; + +/** + * Returns `true` when `err` is an `AbortError`, as generated by `AbortSignal.abort()`. + * + * Note that unlike other errors, `AbortError` cannot be considered a class in itself. + * The best way to create a custom `AbortError` is with `new DOMException(message, 'AbortError')`. + * + * @param err - Object to test. + */ +export function isAbort(err: unknown): err is Error & { name: 'AbortError' }; + +/** + * Returns `true` when `err` is a `TimeoutError`, as generated by `AbortSignal.timeout(delay)`. + * + * Note that unlike other errors, `TimeoutError` cannot be considered a class in itself. + * The best way to create a custom `TimeoutError` is with `new DOMException(message, 'TimeoutError')`. + * + * @param err - Object to test. + */ +export function isTimeout(err: unknown): err is Error & { name: 'TimeoutError' }; diff --git a/package.json b/package.json index 8e0b078..77de38f 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "version": "3.0.1", "repository": "git://github.com/hapijs/bounce", "main": "lib/index.js", + "types": "lib/index.d.ts", "files": [ "lib" ], @@ -18,10 +19,12 @@ "devDependencies": { "@hapi/code": "^9.0.0", "@hapi/eslint-plugin": "^7.0.0", - "@hapi/lab": "^26.0.0" + "@hapi/lab": "^26.0.0", + "@types/node": "^18.19.57", + "typescript": "~5.6.3" }, "scripts": { - "test": "lab -a @hapi/code -t 100 -L", + "test": "lab -a @hapi/code -t 100 -L -Y", "test-cov-html": "lab -a @hapi/code -r html -o coverage.html -L" }, "license": "BSD-3-Clause" diff --git a/test/index.ts b/test/index.ts new file mode 100644 index 0000000..3a2060e --- /dev/null +++ b/test/index.ts @@ -0,0 +1,116 @@ +import * as Bounce from '..'; +import * as Boom from '@hapi/boom'; +import * as Lab from '@hapi/lab'; + +const { expect } = Lab.types; + +// rethrow() + +expect.type(Bounce.rethrow(new Error(), 'system')); +expect.type(Bounce.rethrow(123, 'boom')); +expect.type(Bounce.rethrow(new TypeError(), ['boom', RangeError, { prop: true }])); +expect.type(Bounce.rethrow(new TypeError(), 'boom', { return: true })); +expect.type(Bounce.rethrow(null, 'boom', { return: true, override: new RangeError() })); +expect.type<(TypeError & { prop: string }) | undefined>(Bounce.rethrow(new TypeError(), 'boom', { return: true, decorate: { prop: 'ok' } })); + +expect.error(Bounce.rethrow(new Error())); +expect.error(Bounce.rethrow(new Error(), 'unknown')); +expect.error(Bounce.rethrow(new Error(), 'boom', true)); +expect.error(Bounce.rethrow(new Error(), 'boom', { unknown: true })); +expect.error(Bounce.rethrow(new Error(), 'boom', { decorate: 123 })); +expect.error(Bounce.rethrow(new Error(), 'boom', { override: {} })); + +// ignore() + +expect.type(Bounce.ignore(new TypeError(), 'system')); +expect.type(Bounce.ignore(new Boom.Boom(), 'boom')); +expect.type(Bounce.ignore(new RangeError(), ['boom', RangeError, { prop: true }])); +expect.type(Bounce.ignore(new TypeError(), 'boom', { return: true })); +expect.type(Bounce.ignore(null, 'boom', { return: true, override: new RangeError() })); +expect.type<(TypeError & { prop: string }) | undefined>(Bounce.ignore(new TypeError(), 'boom', { return: true, decorate: { prop: 'ok' } })); + +expect.error(Bounce.ignore(new Error())); +expect.error(Bounce.ignore(new Error(), 'unknown')); +expect.error(Bounce.ignore(new Error(), 'boom', true)); +expect.error(Bounce.ignore(new Error(), 'boom', { unknown: true })); +expect.error(Bounce.ignore(new Error(), 'boom', { decorate: 123 })); +expect.error(Bounce.ignore(new Error(), 'boom', { override: {} })); + +// background() + +expect.type>(Bounce.background(async () => undefined, 'ignore', 'system', { decorate: { a: true } })); +expect.type>(Bounce.background(async () => undefined, 'rethrow', [RangeError], { return: true })); +expect.type>(Bounce.background(async () => undefined, undefined, undefined, { return: true, override: new TypeError() })); + +expect.error(Bounce.background()); +expect.error(Bounce.background(true, 'unknown')); + +// isBoom() + +expect.type(Bounce.isBoom(new Error())); +expect.type(Bounce.isBoom({})); +{ + const obj = {}; + if (Bounce.isBoom(obj)) { + expect.type(obj); // Narrows type + } +} + +expect.error(Bounce.isBoom()); + +// isError() + +expect.type(Bounce.isError(new Error())); +expect.type(Bounce.isError(true)); +{ + const obj = {}; + if (Bounce.isError(obj)) { + expect.type(obj); // Narrows type + } +} + +expect.error(Bounce.isError()); + +// isSystem() + +expect.type(Bounce.isSystem(new Error())); +{ + const err = new TypeError(); + if (Bounce.isSystem(err)) { + expect.type(err); // Does not narrow type + } +} +{ + const obj = {}; + if (Bounce.isSystem(obj)) { + expect.type(obj); // Narrows type + } +} + +expect.error(Bounce.isSystem()); + +// isAbort() + +expect.type(Bounce.isAbort(0)); +{ + const err = AbortSignal.abort().reason as any; + if (Bounce.isAbort(err)) { + expect.type(err); // Narrows type + expect.type<'AbortError'>(err.name); // Narrows name + } +} + +expect.error(Bounce.isAbort()); + +// isTimeout() + +expect.type(Bounce.isTimeout(0)); +{ + const err = AbortSignal.timeout(1).reason as any; + if (Bounce.isTimeout(err)) { + expect.type(err); // Narrows type + expect.type<'TimeoutError'>(err.name); // Narrows name + } +} + +expect.error(Bounce.isTimeout()); From f9b17cee856bc3f1c4ffc2a30204b2ba08188e79 Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Wed, 23 Oct 2024 16:15:46 +0200 Subject: [PATCH 06/22] Fix background() missing return option support --- lib/index.js | 2 +- test/index.js | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index 7854ba1..357d850 100755 --- a/lib/index.js +++ b/lib/index.js @@ -78,7 +78,7 @@ exports.background = async function (operation, action = 'rethrow', types = 'sys } } catch (err) { - exports[action](err, types, options); + return exports[action](err, types, options); } }; diff --git a/test/index.js b/test/index.js index 3f28ea4..b617e98 100755 --- a/test/index.js +++ b/test/index.js @@ -427,6 +427,19 @@ describe('Bounce', () => { Bounce.background(test, 'rethrow', 'system'); }); + + it('supports the return option', async () => { + + const test = async () => { + + await Hoek.wait(1); + throw new SyntaxError('Something'); + }; + + const res = await Bounce.background(test(), 'rethrow', 'system', { return: true }); + expect(res).to.exist(); + expect(res).to.be.an.error(SyntaxError); + }); }); describe('isBoom()', () => { From c2ee4d7c71d339e740d89e7f84664a17655ddcb9 Mon Sep 17 00:00:00 2001 From: Nicolas Morel Date: Wed, 23 Oct 2024 17:47:44 +0200 Subject: [PATCH 07/22] 3.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 23c8ac9..55efb47 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@hapi/bounce", "description": "Selective error catching and rewrite rules", - "version": "3.0.1", + "version": "3.0.2", "repository": "git://github.com/hapijs/bounce", "main": "lib/index.js", "files": [ From 27c8b50cc7526062a5a2a1cbe7d4b321e6b73404 Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Wed, 27 Aug 2025 14:40:52 +0200 Subject: [PATCH 08/22] Narrow error type for ignore() --- lib/index.d.ts | 4 ++++ test/index.ts | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/lib/index.d.ts b/lib/index.d.ts index 2ac152b..6c7b11a 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -63,6 +63,10 @@ export function ignore(err: any, types: T export function ignore(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, decorate: D }): (T & D) | undefined; export function ignore(err: any, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, override: E }): E | undefined; export function ignore(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true }): T | undefined; +export function ignore(err: any, types: 'boom', options: { return?: false | undefined }): asserts err is Boom; +export function ignore(err: any, types: 'boom'): asserts err is Boom; +export function ignore(err: any, types: TypeRule | TypeRule[], options: { return?: false | undefined }): asserts err is Error; +export function ignore(err: any, types: TypeRule | TypeRule[]): asserts err is Error; export function ignore(err: any, types: TypeRule | TypeRule[], options?: BounceOptions): void; type Action = 'rethrow' | 'ignore'; diff --git a/test/index.ts b/test/index.ts index 3a2060e..bfa0c0b 100644 --- a/test/index.ts +++ b/test/index.ts @@ -29,6 +29,20 @@ expect.type(Bounce.ignore(new TypeError(), 'boom', { retu expect.type(Bounce.ignore(null, 'boom', { return: true, override: new RangeError() })); expect.type<(TypeError & { prop: string }) | undefined>(Bounce.ignore(new TypeError(), 'boom', { return: true, decorate: { prop: 'ok' } })); +// Narrows the error type + +{ + const error = new TypeError() as any; + Bounce.ignore(error, TypeError); + expect.type(error); +} + +{ + const error = new Boom.Boom() as any; + Bounce.ignore(error, 'boom'); + expect.type(error); +} + expect.error(Bounce.ignore(new Error())); expect.error(Bounce.ignore(new Error(), 'unknown')); expect.error(Bounce.ignore(new Error(), 'boom', true)); From 566e70cd2232a52087f1133b8ce7ddfa66a963f3 Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Thu, 28 Aug 2025 13:26:57 +0200 Subject: [PATCH 09/22] Make more web usage friendly --- lib/assertions.js | 17 +++++++++++++++++ lib/index.js | 16 +++++----------- 2 files changed, 22 insertions(+), 11 deletions(-) create mode 100644 lib/assertions.js diff --git a/lib/assertions.js b/lib/assertions.js new file mode 100644 index 0000000..e07a576 --- /dev/null +++ b/lib/assertions.js @@ -0,0 +1,17 @@ +'use strict'; + +const Assert = require('assert'); + +const AssertError = require('@hapi/hoek/assertError'); + + +module.exports = [ + + // Node + + Assert.AssertionError, + + // Hoek + + AssertError +]; diff --git a/lib/index.js b/lib/index.js index 357d850..e77195e 100755 --- a/lib/index.js +++ b/lib/index.js @@ -1,9 +1,9 @@ 'use strict'; -const Assert = require('assert'); - const Boom = require('@hapi/boom'); -const Hoek = require('@hapi/hoek'); +const Contain = require('@hapi/hoek/contain'); + +const Assertions = require('./assertions'); const internals = { @@ -18,13 +18,7 @@ const internals = { TypeError, URIError, - // Node - - Assert.AssertionError, - - // Hoek - - Hoek.AssertError + ...Assertions ] }; @@ -135,7 +129,7 @@ internals.match = function (err, types) { } } else if (typeof type === 'object') { - if (Hoek.contain(err, type, { deep: true, part: true })) { + if (Contain(err, type, { deep: true, part: true })) { return true; } } From 0683e812761b3aacccb104cebf4c15c7e5e64efa Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Thu, 28 Aug 2025 13:50:03 +0200 Subject: [PATCH 10/22] Use prototype manipulation to enable much faster system check --- lib/index.js | 31 ++++++++++++++++++++++++++++++- test/index.js | 20 ++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index e77195e..ad58580 100755 --- a/lib/index.js +++ b/lib/index.js @@ -7,6 +7,7 @@ const Assertions = require('./assertions'); const internals = { + symbol: Symbol('SystemError'), system: [ // JavaScript @@ -23,6 +24,30 @@ const internals = { }; +internals.slow = (() => { + + // Add hidden property to prototype that all instances will have + + const slow = []; + const defaultHasInstance = Object[Symbol.hasInstance]; + for (const system of internals.system) { + if (system[Symbol.hasInstance] !== defaultHasInstance) { // Skip classes that use custom instanceof checks + slow.push(system); + continue; + } + + Object.defineProperty(system.prototype, internals.symbol, { + configurable: false, + enumerable: false, + writable: false, + value: true + }); + } + + return slow; +})(); + + exports.rethrow = function (err, types, options = {}) { return internals.catch(err, types, options, true); @@ -99,7 +124,11 @@ exports.isSystem = function (err) { return false; } - for (const system of internals.system) { + if (err[internals.symbol] === true) { + return true; + } + + for (const system of internals.slow) { if (err instanceof system) { return true; } diff --git a/test/index.js b/test/index.js index b617e98..c4be694 100755 --- a/test/index.js +++ b/test/index.js @@ -1,5 +1,20 @@ 'use strict'; +// Add custom assertion to assertion list. Must be done before requiring Bounce + +class HasInstanceCheckError { + + static [Symbol.hasInstance](instance) { + + return instance.message === 'custom-error'; + } +} + +const Assertions = require('../lib/assertions'); + +Assertions.push(HasInstanceCheckError); + + const Assert = require('assert'); const Code = require('@hapi/code'); @@ -520,6 +535,11 @@ describe('Bounce', () => { expect(Bounce.isSystem(new Assert.AssertionError({}))).to.be.true(); }); + it('identifies custom assertion Error as system', () => { + + expect(Bounce.isSystem(new Error('custom-error'))).to.be.true(); + }); + it('identifies hoek Error as system', () => { expect(Bounce.isSystem(new Hoek.AssertError([]))).to.be.true(); From e6dbe8334a19e05e3e623b02c8395389145b4c09 Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Thu, 28 Aug 2025 11:43:10 +0200 Subject: [PATCH 11/22] Remove redundant isBoom() and 'boom' type --- API.md | 5 ----- lib/index.js | 12 ++---------- package.json | 2 +- test/index.js | 45 +++++++++++---------------------------------- 4 files changed, 14 insertions(+), 50 deletions(-) diff --git a/API.md b/API.md index d1cc06c..48d1d1b 100644 --- a/API.md +++ b/API.md @@ -81,7 +81,6 @@ Throws the error passed if it matches any of the specified rules where: - `type` - a single item or an array of items of: - An error constructor (e.g. `SyntaxError`). - `'system'` - matches any languange native error or node assertions. - - `'boom'` - matches [**boom**](https://github.com/hapijs/boom) errors. - an object where each property is compared with the error and must match the error property value. All the properties in the object must match the error but do not need to include all the error properties. @@ -106,10 +105,6 @@ actions where: - `types` - same as the `types` argument passed to `rethrow()` or `ignore()`. Defaults to `'system'`. - `options` - same as the `options` argument passed to `rethrow()` or `ignore()`. -### `isBoom(err)` - -Returns `true` when `err` is a [**boom**](https://github.com/hapijs/boom) error. - ### `isError(err)` Returns `true` when `err` is an error. diff --git a/lib/index.js b/lib/index.js index ad58580..8b3704c 100755 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,5 @@ 'use strict'; -const Boom = require('@hapi/boom'); const Contain = require('@hapi/hoek/contain'); const Assertions = require('./assertions'); @@ -102,12 +101,6 @@ exports.background = async function (operation, action = 'rethrow', types = 'sys }; -exports.isBoom = function (err) { - - return Boom.isBoom(err); -}; - - exports.isError = function (err) { return err instanceof Error; @@ -120,7 +113,7 @@ exports.isSystem = function (err) { return false; } - if (err.isBoom) { + if (err.isBoom) { // Boom errors can be instanceof system errors return false; } @@ -139,8 +132,7 @@ exports.isSystem = function (err) { internals.rules = { - system: exports.isSystem, - boom: exports.isBoom + system: exports.isSystem }; diff --git a/package.json b/package.json index 61c606f..55e1973 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,10 @@ "catch" ], "dependencies": { - "@hapi/boom": "^10.0.1", "@hapi/hoek": "^11.0.4" }, "devDependencies": { + "@hapi/boom": "^10.0.1", "@hapi/code": "^9.0.0", "@hapi/eslint-plugin": "^7.0.0", "@hapi/lab": "^26.0.0" diff --git a/test/index.js b/test/index.js index c4be694..53e2fce 100755 --- a/test/index.js +++ b/test/index.js @@ -74,7 +74,7 @@ describe('Bounce', () => { it('rethrows only boom errors', () => { try { - Bounce.rethrow(new Error('Something'), 'boom'); + Bounce.rethrow(new Error('Something'), Boom.Boom); } catch (err) { var error1 = err; @@ -83,7 +83,7 @@ describe('Bounce', () => { expect(error1).to.not.exist(); try { - Bounce.rethrow(Boom.badRequest('Something'), 'boom'); + Bounce.rethrow(Boom.badRequest('Something'), Boom.Boom); } catch (err) { var error2 = err; @@ -95,7 +95,7 @@ describe('Bounce', () => { it('rethrows only boom/system errors', () => { try { - Bounce.rethrow(new Error('Something'), ['boom', 'system']); + Bounce.rethrow(new Error('Something'), [Boom.Boom, 'system']); } catch (err) { var error1 = err; @@ -104,7 +104,7 @@ describe('Bounce', () => { expect(error1).to.not.exist(); try { - Bounce.rethrow(Boom.badRequest('Something'), ['boom', 'system']); + Bounce.rethrow(Boom.badRequest('Something'), [Boom.Boom, 'system']); } catch (err) { var error2 = err; @@ -113,7 +113,7 @@ describe('Bounce', () => { expect(error2).to.be.an.error('Something'); try { - Bounce.rethrow(new SyntaxError('Something'), ['boom', 'system']); + Bounce.rethrow(new SyntaxError('Something'), [Boom.Boom, 'system']); } catch (err) { var error3 = err; @@ -283,7 +283,7 @@ describe('Bounce', () => { it('ignores boom errors', () => { try { - Bounce.ignore(new Error('Something'), 'boom'); + Bounce.ignore(new Error('Something'), Boom.Boom); } catch (err) { var error1 = err; @@ -292,7 +292,7 @@ describe('Bounce', () => { expect(error1).to.be.an.error('Something', Error); try { - Bounce.ignore(Boom.badRequest('Something'), 'boom'); + Bounce.ignore(Boom.badRequest('Something'), Boom.Boom); } catch (err) { var error2 = err; @@ -304,7 +304,7 @@ describe('Bounce', () => { it('ignores boom/system errors', () => { try { - Bounce.ignore(new Error('Something'), ['boom', 'system']); + Bounce.ignore(new Error('Something'), [Boom.Boom, 'system']); } catch (err) { var error1 = err; @@ -313,7 +313,7 @@ describe('Bounce', () => { expect(error1).to.be.an.error('Something', Error); try { - Bounce.ignore(Boom.badRequest('Something'), ['boom', 'system']); + Bounce.ignore(Boom.badRequest('Something'), [Boom.Boom, 'system']); } catch (err) { var error2 = err; @@ -322,7 +322,7 @@ describe('Bounce', () => { expect(error2).to.not.exist(); try { - Bounce.ignore(new ReferenceError('Something'), ['boom', 'system']); + Bounce.ignore(new ReferenceError('Something'), [Boom.Boom, 'system']); } catch (err) { var error3 = err; @@ -457,29 +457,6 @@ describe('Bounce', () => { }); }); - describe('isBoom()', () => { - - it('identifies Boom as Boom', () => { - - expect(Bounce.isBoom(Boom.badRequest())).to.be.true(); - }); - - it('identifies EvalError as non-boom', () => { - - expect(Bounce.isBoom(new EvalError())).to.be.false(); - }); - - it('identifies object as non-boom', () => { - - expect(Bounce.isBoom({})).to.be.false(); - }); - - it('identifies object with isBoom as non-boom', () => { - - expect(Bounce.isBoom({ isBoom: true })).to.be.false(); - }); - }); - describe('isError()', () => { it('identifies Error as error', () => { @@ -494,7 +471,7 @@ describe('Bounce', () => { it('identifies object as non-error', () => { - expect(Bounce.isBoom({})).to.be.false(); + expect(Bounce.isError({})).to.be.false(); }); }); From d6daed33fcb2cb9209a04a3c32d908586e9cb48c Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Thu, 28 Aug 2025 11:45:29 +0200 Subject: [PATCH 12/22] Remove redundant isError() --- API.md | 4 ---- lib/index.js | 6 ------ test/index.js | 18 ------------------ 3 files changed, 28 deletions(-) diff --git a/API.md b/API.md index 48d1d1b..2350cb6 100644 --- a/API.md +++ b/API.md @@ -105,10 +105,6 @@ actions where: - `types` - same as the `types` argument passed to `rethrow()` or `ignore()`. Defaults to `'system'`. - `options` - same as the `options` argument passed to `rethrow()` or `ignore()`. -### `isError(err)` - -Returns `true` when `err` is an error. - ### `isSystem(err)` Return `true` when `err` is one of: diff --git a/lib/index.js b/lib/index.js index 8b3704c..f0ac135 100755 --- a/lib/index.js +++ b/lib/index.js @@ -101,12 +101,6 @@ exports.background = async function (operation, action = 'rethrow', types = 'sys }; -exports.isError = function (err) { - - return err instanceof Error; -}; - - exports.isSystem = function (err) { if (!err) { diff --git a/test/index.js b/test/index.js index 53e2fce..6f83a7c 100755 --- a/test/index.js +++ b/test/index.js @@ -457,24 +457,6 @@ describe('Bounce', () => { }); }); - describe('isError()', () => { - - it('identifies Error as error', () => { - - expect(Bounce.isError(new Error())).to.be.true(); - }); - - it('identifies Boom as error', () => { - - expect(Bounce.isError(Boom.badRequest())).to.be.true(); - }); - - it('identifies object as non-error', () => { - - expect(Bounce.isError({})).to.be.false(); - }); - }); - describe('isSystem()', () => { it('identifies EvalError as system', () => { From 2b5c1829f489c0cef4cd20f1ca65ef5ef5d78e85 Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Thu, 28 Aug 2025 13:06:24 +0200 Subject: [PATCH 13/22] Enforce that "types" option is required --- lib/index.js | 8 ++++---- test/index.js | 34 +++++++++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/lib/index.js b/lib/index.js index f0ac135..e4a0373 100755 --- a/lib/index.js +++ b/lib/index.js @@ -61,6 +61,10 @@ exports.ignore = function (err, types, options = {}) { internals.catch = function (err, types, options, match) { + if (!types) { + throw new TypeError('Missing or invalid "types" argument'); + } + if (internals.match(err, types) !== match) { return; } @@ -132,10 +136,6 @@ internals.rules = { internals.match = function (err, types) { - if (!types) { - return true; - } - types = Array.isArray(types) ? types : [types]; for (const type of types) { if (typeof type === 'string') { diff --git a/test/index.js b/test/index.js index 6f83a7c..f5acee2 100755 --- a/test/index.js +++ b/test/index.js @@ -35,7 +35,7 @@ describe('Bounce', () => { describe('rethrow()', () => { - it('rethrows all errors', () => { + it('throws TypeError on missing types', () => { const orig = new Error('Something'); @@ -46,8 +46,8 @@ describe('Bounce', () => { var error = err; } - expect(error).to.shallow.equal(orig); - expect(error).to.be.an.error('Something'); + expect(error).to.not.shallow.equal(orig); + expect(error).to.be.an.error(TypeError); }); it('rethrows only system errors', () => { @@ -255,6 +255,19 @@ describe('Bounce', () => { expect(error).to.shallow.equal(orig); expect(error).to.be.an.error('Something'); }); + + it('preserves non-errors', () => { + + try { + Bounce.rethrow('error', []); + } + catch (err) { + var error = err; + } + + expect(error).to.not.exist(); + }); + }); describe('ignore()', () => { @@ -330,6 +343,21 @@ describe('Bounce', () => { expect(error3).to.not.exist(); }); + + it('always throws non-errors', () => { + + const orig = 'error'; + + try { + Bounce.ignore(orig, []); + } + catch (err) { + var error = err; + } + + expect(error).to.shallow.equal(orig); + }); + }); describe('background()', () => { From 13cc0ceb4f5af98123dcd2cbd66aaf04267ce093 Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Thu, 28 Aug 2025 13:11:39 +0200 Subject: [PATCH 14/22] Only match objects that inherit Error --- lib/index.js | 16 ++++++++++++++-- test/index.js | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index e4a0373..2f0c5cb 100755 --- a/lib/index.js +++ b/lib/index.js @@ -107,10 +107,18 @@ exports.background = async function (operation, action = 'rethrow', types = 'sys exports.isSystem = function (err) { - if (!err) { + if (!(err instanceof Error)) { return false; } + return internals.isSystemError(err); +}; + + +internals.isSystemError = function (err) { + + // 'err' is always instanceof Error + if (err.isBoom) { // Boom errors can be instanceof system errors return false; } @@ -130,12 +138,16 @@ exports.isSystem = function (err) { internals.rules = { - system: exports.isSystem + system: internals.isSystemError }; internals.match = function (err, types) { + if (!(err instanceof Error)) { + return false; + } + types = Array.isArray(types) ? types : [types]; for (const type of types) { if (typeof type === 'string') { diff --git a/test/index.js b/test/index.js index f5acee2..66d9dcd 100755 --- a/test/index.js +++ b/test/index.js @@ -212,6 +212,20 @@ describe('Bounce', () => { expect(error2).to.be.an.error('Something'); }); + it('ignores non-errors matching a pattern', () => { + + const nonErr = { x: 1 }; + + try { + Bounce.rethrow(nonErr, { x: 1 }); + } + catch (err) { + var error = err; + } + + expect(error).to.not.exist(); + }); + it('rethrows a decorated error', () => { const orig = new Error('Something'); From 42e41346524662c23581d3f74fac5b211206731b Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Sun, 31 Aug 2025 12:41:09 +0200 Subject: [PATCH 15/22] Try to narrow all types for Hoek.ignore() --- lib/index.d.ts | 16 +++++++++++----- test/index.ts | 14 +++++++++++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/lib/index.d.ts b/lib/index.d.ts index c16640e..1770a0f 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -12,6 +12,12 @@ type TypeRule = 'system' | 'abort' | 'timeout' | ErrorConstructor | { [key: Prop type Decoration = { [key: string]: any }; +type TypeMapping = + T extends 'abort' ? Error & { name: 'AbortError' } : + T extends 'timeout' ? Error & { name: 'TimeoutError' } : + T extends abstract new (...args: any) => Error ? InstanceType : + Error; + interface BounceOptions { /** @@ -60,8 +66,8 @@ export function ignore(err: any, types: T export function ignore(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, decorate: D }): (T & D) | undefined; export function ignore(err: any, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, override: E }): E | undefined; export function ignore(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true }): T | undefined; -export function ignore Error>(err: any, types: T, options: { return?: false | undefined }): asserts err is InstanceType; -export function ignore Error>(err: any, types: T): asserts err is InstanceType; +export function ignore(err: any, types: T, options: { return?: false | undefined }): asserts err is TypeMapping; +export function ignore(err: any, types: T): asserts err is TypeMapping; export function ignore(err: any, types: TypeRule | TypeRule[], options: { return?: false | undefined }): asserts err is Error; export function ignore(err: any, types: TypeRule | TypeRule[]): asserts err is Error; export function ignore(err: any, types: TypeRule | TypeRule[], options?: BounceOptions): void; @@ -95,7 +101,7 @@ export function background(operation: any, action?: Action, types?: TypeRule | T * * @param err - Object to test. */ -export function isSystem(err: unknown): err is Error; +export function isSystem(err: unknown): err is TypeMapping<'system'>; /** * Returns `true` when `err` is an `AbortError`, as generated by `AbortSignal.abort()`. @@ -105,7 +111,7 @@ export function isSystem(err: unknown): err is Error; * * @param err - Object to test. */ -export function isAbort(err: unknown): err is Error & { name: 'AbortError' }; +export function isAbort(err: unknown): err is TypeMapping<'abort'>; /** * Returns `true` when `err` is a `TimeoutError`, as generated by `AbortSignal.timeout(delay)`. @@ -115,4 +121,4 @@ export function isAbort(err: unknown): err is Error & { name: 'AbortError' }; * * @param err - Object to test. */ -export function isTimeout(err: unknown): err is Error & { name: 'TimeoutError' }; +export function isTimeout(err: unknown): err is TypeMapping<'timeout'>; diff --git a/test/index.ts b/test/index.ts index 1ed1c8c..e4de038 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1,5 +1,6 @@ import * as Bounce from '..'; import * as Boom from '@hapi/boom'; +import * as Hoek from '@hapi/hoek'; import * as Lab from '@hapi/lab'; const { expect } = Lab.types; @@ -96,6 +97,9 @@ expect.type(Bounce.isAbort(0)); expect.type(err); // Narrows type expect.type<'AbortError'>(err.name); // Narrows name } + + Bounce.ignore(err, 'abort'); + expect.type<'AbortError'>(err.name); // Narrows name } expect.error(Bounce.isAbort()); @@ -104,11 +108,19 @@ expect.error(Bounce.isAbort()); expect.type(Bounce.isTimeout(0)); { - const err = AbortSignal.timeout(1).reason as any; + const timeout = AbortSignal.timeout(1); + const err = await new Promise((resolve) => { + + timeout.onabort = () => resolve(timeout.reason); + }); + if (Bounce.isTimeout(err)) { expect.type(err); // Narrows type expect.type<'TimeoutError'>(err.name); // Narrows name } + + Bounce.ignore(err, 'timeout'); + expect.type<'TimeoutError'>(err.name); // Narrows name } expect.error(Bounce.isTimeout()); From 6ef478c9237f87a60c683164693747c38d752c4a Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Sun, 31 Aug 2025 13:21:17 +0200 Subject: [PATCH 16/22] Cleanup typings --- lib/index.d.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/index.d.ts b/lib/index.d.ts index 1770a0f..356a702 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -23,18 +23,23 @@ interface BounceOptions { /** * An object which is assigned to the `err`, copying the properties onto the error. */ - decorate?: { [key: string]: any }; + decorate?: { [key: string]: any } | undefined; /** * An error used to override `err` when `err` matches. * If used with `decorate`, the `override` object is modified. */ - override?: Error; + override?: Error | undefined; /** * If `true`, the error is returned instead of thrown. Defaults to `false`. */ - return?: boolean; + return?: boolean | undefined; + + /** + * An `AbortSignal`. Throws `signal.reason` if it has already been aborted. + */ + signal?: AbortSignal | undefined; } /** From 7aacf032e7595e5b0e86c749e38d225d463612ba Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Sun, 31 Aug 2025 13:18:22 +0200 Subject: [PATCH 17/22] Add a "strict" option --- API.md | 1 + lib/index.d.ts | 5 +++++ lib/index.js | 19 +++++++++++++++---- test/index.js | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/API.md b/API.md index 12b262a..03250ef 100644 --- a/API.md +++ b/API.md @@ -91,6 +91,7 @@ Throws the error passed if it matches any of the specified rules where: - `override` - an error used to override `err` when `err` matches. If used with `decorate`, the `override` object is modified. - `return` - if `true`, the error is returned instead of thrown. Defaults to `false`. + - `strict` - if `true`, any non-`Error` `err` throws a `TypeError`. Defaults to `false`. - `signal` - an `AbortSignal`. Throws `signal.reason` if it has already been aborted. ### `ignore(err, types, [options])` diff --git a/lib/index.d.ts b/lib/index.d.ts index 356a702..a8c9a6b 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -36,6 +36,11 @@ interface BounceOptions { */ return?: boolean | undefined; + /** + * If `true`, any non-`Error` `err` throws a`TypeError`. Defaults to `false`. + */ + strict?: boolean | undefined; + /** * An `AbortSignal`. Throws `signal.reason` if it has already been aborted. */ diff --git a/lib/index.js b/lib/index.js index c287050..86bbafe 100755 --- a/lib/index.js +++ b/lib/index.js @@ -69,10 +69,25 @@ internals.catch = function (err, types, options, match) { throw options.signal.reason ?? new DOMException('This operation was aborted', 'AbortError'); } + const isErrorType = err instanceof Error; + if (!isErrorType) { + if (options.strict) { + throw new TypeError('Argument is not an Error', { cause: err }); + } + + return match ? undefined : internals.matched(err, options); + } + if (internals.match(err, types) !== match) { return; } + return internals.matched(err, options); +}; + + +internals.matched = function (err, options) { + // Error replacement if (options.override) { @@ -174,10 +189,6 @@ internals.rules = { internals.match = function (err, types) { - if (!(err instanceof Error)) { - return false; - } - types = Array.isArray(types) ? types : [types]; for (const type of types) { if (typeof type === 'string') { diff --git a/test/index.js b/test/index.js index 602d1c0..33e4451 100755 --- a/test/index.js +++ b/test/index.js @@ -376,6 +376,31 @@ describe('Bounce', () => { expect(error).to.shallow.equal(orig); expect(error).to.be.an.error('Something'); }); + + it('always throws TypeError for non-errors when strict', () => { + + const orig = 'error'; + + try { + Bounce.rethrow(orig, [], { strict: true }); + } + catch (err) { + var error1 = err; + } + + expect(error1).to.be.an.error(TypeError); + expect(error1.cause).to.shallow.equal(orig); + + try { + Bounce.rethrow(orig, [], { strict: true }); + } + catch (err) { + var error2 = err; + } + + expect(error2).to.be.an.error(TypeError); + expect(error2.cause).to.shallow.equal(orig); + }); }); describe('ignore()', () => { @@ -497,6 +522,31 @@ describe('Bounce', () => { expect(error).to.shallow.equal(orig); expect(error).to.be.an.error('Something'); }); + + it('always throws TypeError for non-errors when strict', () => { + + const orig = 'error'; + + try { + Bounce.ignore(orig, [], { strict: true }); + } + catch (err) { + var error1 = err; + } + + expect(error1).to.be.an.error(TypeError); + expect(error1.cause).to.shallow.equal(orig); + + try { + Bounce.ignore(orig, [], { strict: true }); + } + catch (err) { + var error2 = err; + } + + expect(error2).to.be.an.error(TypeError); + expect(error2.cause).to.shallow.equal(orig); + }); }); describe('background()', () => { From d46788b237754733a2e6577ccb153a65efe9f300 Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Sun, 31 Aug 2025 13:31:45 +0200 Subject: [PATCH 18/22] Make strict mode the default --- API.md | 2 +- lib/index.d.ts | 2 +- lib/index.js | 2 +- test/index.js | 6 +++--- test/index.ts | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/API.md b/API.md index 03250ef..50440cc 100644 --- a/API.md +++ b/API.md @@ -91,7 +91,7 @@ Throws the error passed if it matches any of the specified rules where: - `override` - an error used to override `err` when `err` matches. If used with `decorate`, the `override` object is modified. - `return` - if `true`, the error is returned instead of thrown. Defaults to `false`. - - `strict` - if `true`, any non-`Error` `err` throws a `TypeError`. Defaults to `false`. + - `strict` - if `true`, any non-`Error` `err` throws a `TypeError`. Defaults to `true`. - `signal` - an `AbortSignal`. Throws `signal.reason` if it has already been aborted. ### `ignore(err, types, [options])` diff --git a/lib/index.d.ts b/lib/index.d.ts index a8c9a6b..27b29e0 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -37,7 +37,7 @@ interface BounceOptions { return?: boolean | undefined; /** - * If `true`, any non-`Error` `err` throws a`TypeError`. Defaults to `false`. + * If `true`, any non-`Error` `err` throws a`TypeError`. Defaults to `true`. */ strict?: boolean | undefined; diff --git a/lib/index.js b/lib/index.js index 86bbafe..e7253ce 100755 --- a/lib/index.js +++ b/lib/index.js @@ -71,7 +71,7 @@ internals.catch = function (err, types, options, match) { const isErrorType = err instanceof Error; if (!isErrorType) { - if (options.strict) { + if (options.strict !== false) { throw new TypeError('Argument is not an Error', { cause: err }); } diff --git a/test/index.js b/test/index.js index 33e4451..c121894 100755 --- a/test/index.js +++ b/test/index.js @@ -263,7 +263,7 @@ describe('Bounce', () => { const nonErr = { x: 1 }; try { - Bounce.rethrow(nonErr, { x: 1 }); + Bounce.rethrow(nonErr, { x: 1 }, { strict: false }); } catch (err) { var error = err; @@ -319,7 +319,7 @@ describe('Bounce', () => { it('preserves non-errors', () => { try { - Bounce.rethrow('error', []); + Bounce.rethrow('error', [], { strict: false }); } catch (err) { var error = err; @@ -482,7 +482,7 @@ describe('Bounce', () => { const orig = 'error'; try { - Bounce.ignore(orig, []); + Bounce.ignore(orig, [], { strict: false }); } catch (err) { var error = err; diff --git a/test/index.ts b/test/index.ts index e4de038..90bf410 100644 --- a/test/index.ts +++ b/test/index.ts @@ -8,10 +8,10 @@ const { expect } = Lab.types; // rethrow() expect.type(Bounce.rethrow(new Error(), 'system')); -expect.type(Bounce.rethrow(123, Boom.Boom)); +expect.type(Bounce.rethrow(123, Boom.Boom, { strict: false })); expect.type(Bounce.rethrow(new TypeError(), [Boom.Boom, RangeError, { prop: true }])); expect.type(Bounce.rethrow(new TypeError(), Boom.Boom, { return: true })); -expect.type(Bounce.rethrow(null, Boom.Boom, { return: true, override: new RangeError() })); +expect.type(Bounce.rethrow(null, Boom.Boom, { strict: false, return: true, override: new RangeError() })); expect.type<(TypeError & { prop: string }) | undefined>(Bounce.rethrow(new TypeError(), Boom.Boom, { return: true, decorate: { prop: 'ok' } })); expect.error(Bounce.rethrow(new Error())); @@ -27,7 +27,7 @@ expect.type(Bounce.ignore(new TypeError(), 'system')); expect.type(Bounce.ignore(new Boom.Boom(), Boom.Boom)); expect.type(Bounce.ignore(new RangeError(), [Boom.Boom, RangeError, { prop: true }])); expect.type(Bounce.ignore(new TypeError(), Boom.Boom, { return: true })); -expect.type(Bounce.ignore(null, Boom.Boom, { return: true, override: new RangeError() })); +expect.type(Bounce.ignore(null, Boom.Boom, { return: true, override: new RangeError(), strict: false })); expect.type<(TypeError & { prop: string }) | undefined>(Bounce.ignore(new TypeError(), Boom.Boom, { return: true, decorate: { prop: 'ok' } })); // Narrows the error type From 735d6248ebe0cfded9fbe882e76d9dea88431d00 Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Sun, 31 Aug 2025 14:10:36 +0200 Subject: [PATCH 19/22] Narrow rethrow() error when strict --- lib/index.d.ts | 1 + test/index.ts | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/lib/index.d.ts b/lib/index.d.ts index 27b29e0..66e2fe2 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -60,6 +60,7 @@ export function rethrow(err: any, types: export function rethrow(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, decorate: D }): (T & D) | undefined; export function rethrow(err: any, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, override: E }): E | undefined; export function rethrow(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true }): T | undefined; +export function rethrow(err: any, types: TypeRule | TypeRule[], options?: { strict: true }): asserts err is Error; export function rethrow(err: any, types: TypeRule | TypeRule[], options?: BounceOptions): void; /** diff --git a/test/index.ts b/test/index.ts index 90bf410..2bb132f 100644 --- a/test/index.ts +++ b/test/index.ts @@ -14,6 +14,26 @@ expect.type(Bounce.rethrow(new TypeError(), Boom.Boom, { expect.type(Bounce.rethrow(null, Boom.Boom, { strict: false, return: true, override: new RangeError() })); expect.type<(TypeError & { prop: string }) | undefined>(Bounce.rethrow(new TypeError(), Boom.Boom, { return: true, decorate: { prop: 'ok' } })); +// Narrows the error type + +{ + const error = new Error() as unknown; + Bounce.rethrow(error, 'system'); + expect.type(error); +} + +{ + const error = new Error() as unknown; + Bounce.rethrow(error, 'system', { strict: true } as { strict: boolean }); + expect.type(error); +} + +{ + const error = 123; + Bounce.rethrow(error, 'system', { strict: false }); + expect.type(error); +} + expect.error(Bounce.rethrow(new Error())); expect.error(Bounce.rethrow(new Error(), 'unknown')); expect.error(Bounce.rethrow(new Error(), Boom.Boom, true)); From f9b5e907b45f763f59829b0e1171c76c6f19dc48 Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Sun, 31 Aug 2025 14:29:10 +0200 Subject: [PATCH 20/22] Rename ignore to assert --- API.md | 10 +++++----- lib/index.d.ts | 28 ++++++++++++++-------------- lib/index.js | 2 +- test/index.js | 26 +++++++++++++------------- test/index.ts | 38 +++++++++++++++++++------------------- 5 files changed, 52 insertions(+), 52 deletions(-) diff --git a/API.md b/API.md index 50440cc..73e6605 100644 --- a/API.md +++ b/API.md @@ -94,20 +94,20 @@ Throws the error passed if it matches any of the specified rules where: - `strict` - if `true`, any non-`Error` `err` throws a `TypeError`. Defaults to `true`. - `signal` - an `AbortSignal`. Throws `signal.reason` if it has already been aborted. -### `ignore(err, types, [options])` +### `assert(err, types, [options])` The opposite action of `rethrow()`. Ignores any errors matching the specified `types`. Any error not matching is thrown after applying the `options`. ### `background(operation, [action], [types], [options])` -Awaits for the value to resolve in the background and then apply either the `rethrow()` or `ignore()` +Awaits for the value to resolve in the background and then apply either the `rethrow()` or `assert()` actions where: - `operation` - a function, promise, or value that is `await`ed on inside a `try...catch` and any error thrown processed by the `action` rule. -- `action` - one of `'rethrow'` or `'ignore'`. Defaults to `'rethrow'`. -- `types` - same as the `types` argument passed to `rethrow()` or `ignore()`. Defaults to `'system'`. -- `options` - same as the `options` argument passed to `rethrow()` or `ignore()`. +- `action` - one of `'rethrow'` or `'assert'`. Defaults to `'rethrow'`. +- `types` - same as the `types` argument passed to `rethrow()` or `assert()`. Defaults to `'system'`. +- `options` - same as the `options` argument passed to `rethrow()` or `assert()`. ### `isSystem(err)` diff --git a/lib/index.d.ts b/lib/index.d.ts index 66e2fe2..6f65944 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -73,26 +73,26 @@ export function rethrow(err: any, types: TypeRule | TypeRule[], options?: Bounce * * @returns possibly an `Error` depending on value of the `return` and `decorate` options. */ -export function ignore(err: any, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, decorate: D, override: E }): (E & D) | undefined; -export function ignore(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, decorate: D }): (T & D) | undefined; -export function ignore(err: any, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, override: E }): E | undefined; -export function ignore(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true }): T | undefined; -export function ignore(err: any, types: T, options: { return?: false | undefined }): asserts err is TypeMapping; -export function ignore(err: any, types: T): asserts err is TypeMapping; -export function ignore(err: any, types: TypeRule | TypeRule[], options: { return?: false | undefined }): asserts err is Error; -export function ignore(err: any, types: TypeRule | TypeRule[]): asserts err is Error; -export function ignore(err: any, types: TypeRule | TypeRule[], options?: BounceOptions): void; +export function assert(err: any, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, decorate: D, override: E }): (E & D) | undefined; +export function assert(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, decorate: D }): (T & D) | undefined; +export function assert(err: any, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, override: E }): E | undefined; +export function assert(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true }): T | undefined; +export function assert(err: any, types: T, options: { return?: false | undefined }): asserts err is TypeMapping; +export function assert(err: any, types: T): asserts err is TypeMapping; +export function assert(err: any, types: TypeRule | TypeRule[], options: { return?: false | undefined }): asserts err is Error; +export function assert(err: any, types: TypeRule | TypeRule[]): asserts err is Error; +export function assert(err: any, types: TypeRule | TypeRule[], options?: BounceOptions): void; -type Action = 'rethrow' | 'ignore'; +type Action = 'rethrow' | 'assert'; /** - * Awaits for the value to resolve in the background and then apply either the {@link rethrow `rethrow()`} or {@link rethrow `ignore()`} action. + * Awaits for the value to resolve in the background and then apply either the {@link rethrow `rethrow()`} or {@link rethrow `assert()`} action. * * @param operation - a function, promise, or value that is `await`ed on inside a `try...catch` * and any error thrown processed by the `action` rule. - * @param action - one of `'rethrow'` or`'ignore'`. Defaults to`'rethrow'`. - * @param types - same as the `types` argument passed to {@link rethrow `rethrow()`} or {@link rethrow `ignore()`}. Defaults to `'system'`. - * @param options - same as the {@link BounceOptions `options`} argument passed to {@link rethrow `rethrow()`} or {@link rethrow `ignore()`}. + * @param action - one of `'rethrow'` or `'assert'`. Defaults to `'rethrow'`. + * @param types - same as the `types` argument passed to {@link rethrow `rethrow()`} or {@link rethrow `assert()`}. Defaults to `'system'`. + * @param options - same as the {@link BounceOptions `options`} argument passed to {@link rethrow `rethrow()`} or {@link rethrow `assert()`}. */ export function background(operation: any, action?: Action, types?: TypeRule | TypeRule[], options?: BounceOptions & { return: true, override: E, decorate: D }): Promise<(E & D) | undefined>; export function background(operation: any, action?: Action, types?: TypeRule | TypeRule[], options?: BounceOptions & { return: true, override: E }): Promise; diff --git a/lib/index.js b/lib/index.js index e7253ce..1ba7227 100755 --- a/lib/index.js +++ b/lib/index.js @@ -53,7 +53,7 @@ exports.rethrow = function (err, types, options = {}) { }; -exports.ignore = function (err, types, options = {}) { +exports.assert = function (err, types, options = {}) { return internals.catch(err, types, options, false); }; diff --git a/test/index.js b/test/index.js index c121894..42a5f51 100755 --- a/test/index.js +++ b/test/index.js @@ -403,12 +403,12 @@ describe('Bounce', () => { }); }); - describe('ignore()', () => { + describe('assert()', () => { it('ignores system errors', () => { try { - Bounce.ignore(new Error('Something'), 'system'); + Bounce.assert(new Error('Something'), 'system'); } catch (err) { var error1 = err; @@ -417,7 +417,7 @@ describe('Bounce', () => { expect(error1).to.be.an.error('Something', Error); try { - Bounce.ignore(new URIError('Something'), 'system'); + Bounce.assert(new URIError('Something'), 'system'); } catch (err) { var error2 = err; @@ -429,7 +429,7 @@ describe('Bounce', () => { it('ignores boom errors', () => { try { - Bounce.ignore(new Error('Something'), Boom.Boom); + Bounce.assert(new Error('Something'), Boom.Boom); } catch (err) { var error1 = err; @@ -438,7 +438,7 @@ describe('Bounce', () => { expect(error1).to.be.an.error('Something', Error); try { - Bounce.ignore(Boom.badRequest('Something'), Boom.Boom); + Bounce.assert(Boom.badRequest('Something'), Boom.Boom); } catch (err) { var error2 = err; @@ -450,7 +450,7 @@ describe('Bounce', () => { it('ignores boom/system errors', () => { try { - Bounce.ignore(new Error('Something'), [Boom.Boom, 'system']); + Bounce.assert(new Error('Something'), [Boom.Boom, 'system']); } catch (err) { var error1 = err; @@ -459,7 +459,7 @@ describe('Bounce', () => { expect(error1).to.be.an.error('Something', Error); try { - Bounce.ignore(Boom.badRequest('Something'), [Boom.Boom, 'system']); + Bounce.assert(Boom.badRequest('Something'), [Boom.Boom, 'system']); } catch (err) { var error2 = err; @@ -468,7 +468,7 @@ describe('Bounce', () => { expect(error2).to.not.exist(); try { - Bounce.ignore(new ReferenceError('Something'), [Boom.Boom, 'system']); + Bounce.assert(new ReferenceError('Something'), [Boom.Boom, 'system']); } catch (err) { var error3 = err; @@ -482,7 +482,7 @@ describe('Bounce', () => { const orig = 'error'; try { - Bounce.ignore(orig, [], { strict: false }); + Bounce.assert(orig, [], { strict: false }); } catch (err) { var error = err; @@ -497,7 +497,7 @@ describe('Bounce', () => { const signal = AbortSignal.abort(new Error('Fail')); try { - Bounce.ignore(orig, 'system', { signal }); + Bounce.assert(orig, 'system', { signal }); } catch (err) { var error = err; @@ -513,7 +513,7 @@ describe('Bounce', () => { const signal = new AbortController().signal; try { - Bounce.ignore(orig, 'system', { signal }); + Bounce.assert(orig, 'system', { signal }); } catch (err) { var error = err; @@ -528,7 +528,7 @@ describe('Bounce', () => { const orig = 'error'; try { - Bounce.ignore(orig, [], { strict: true }); + Bounce.assert(orig, [], { strict: true }); } catch (err) { var error1 = err; @@ -538,7 +538,7 @@ describe('Bounce', () => { expect(error1.cause).to.shallow.equal(orig); try { - Bounce.ignore(orig, [], { strict: true }); + Bounce.assert(orig, [], { strict: true }); } catch (err) { var error2 = err; diff --git a/test/index.ts b/test/index.ts index 2bb132f..7ea1732 100644 --- a/test/index.ts +++ b/test/index.ts @@ -41,26 +41,26 @@ expect.error(Bounce.rethrow(new Error(), Boom.Boom, { unknown: true })); expect.error(Bounce.rethrow(new Error(), Boom.Boom, { decorate: 123 })); expect.error(Bounce.rethrow(new Error(), Boom.Boom, { override: {} })); -// ignore() +// assert() -expect.type(Bounce.ignore(new TypeError(), 'system')); -expect.type(Bounce.ignore(new Boom.Boom(), Boom.Boom)); -expect.type(Bounce.ignore(new RangeError(), [Boom.Boom, RangeError, { prop: true }])); -expect.type(Bounce.ignore(new TypeError(), Boom.Boom, { return: true })); -expect.type(Bounce.ignore(null, Boom.Boom, { return: true, override: new RangeError(), strict: false })); -expect.type<(TypeError & { prop: string }) | undefined>(Bounce.ignore(new TypeError(), Boom.Boom, { return: true, decorate: { prop: 'ok' } })); +expect.type(Bounce.assert(new TypeError(), 'system')); +expect.type(Bounce.assert(new Boom.Boom(), Boom.Boom)); +expect.type(Bounce.assert(new RangeError(), [Boom.Boom, RangeError, { prop: true }])); +expect.type(Bounce.assert(new TypeError(), Boom.Boom, { return: true })); +expect.type(Bounce.assert(null, Boom.Boom, { return: true, override: new RangeError(), strict: false })); +expect.type<(TypeError & { prop: string }) | undefined>(Bounce.assert(new TypeError(), Boom.Boom, { return: true, decorate: { prop: 'ok' } })); // Narrows the error type { const error = new TypeError() as unknown; - Bounce.ignore(error, TypeError); + Bounce.assert(error, TypeError); expect.type(error); } { const error = new Boom.Boom() as unknown; - Bounce.ignore(error, Boom.Boom); + Bounce.assert(error, Boom.Boom); expect.type(error); } @@ -70,20 +70,20 @@ expect.type<(TypeError & { prop: string }) | undefined>(Bounce.ignore(new TypeEr } const error = new TestError() as unknown; - Bounce.ignore(error, TestError); + Bounce.assert(error, TestError); expect.type(error); } -expect.error(Bounce.ignore(new Error())); -expect.error(Bounce.ignore(new Error(), 'unknown')); -expect.error(Bounce.ignore(new Error(), Boom.Boom, true)); -expect.error(Bounce.ignore(new Error(), Boom.Boom, { unknown: true })); -expect.error(Bounce.ignore(new Error(), Boom.Boom, { decorate: 123 })); -expect.error(Bounce.ignore(new Error(), Boom.Boom, { override: {} })); +expect.error(Bounce.assert(new Error())); +expect.error(Bounce.assert(new Error(), 'unknown')); +expect.error(Bounce.assert(new Error(), Boom.Boom, true)); +expect.error(Bounce.assert(new Error(), Boom.Boom, { unknown: true })); +expect.error(Bounce.assert(new Error(), Boom.Boom, { decorate: 123 })); +expect.error(Bounce.assert(new Error(), Boom.Boom, { override: {} })); // background() -expect.type>(Bounce.background(async () => undefined, 'ignore', 'system', { decorate: { a: true } })); +expect.type>(Bounce.background(async () => undefined, 'assert', 'system', { decorate: { a: true } })); expect.type>(Bounce.background(async () => undefined, 'rethrow', [RangeError], { return: true })); expect.type>(Bounce.background(async () => undefined, undefined, undefined, { return: true, override: new TypeError() })); @@ -118,7 +118,7 @@ expect.type(Bounce.isAbort(0)); expect.type<'AbortError'>(err.name); // Narrows name } - Bounce.ignore(err, 'abort'); + Bounce.assert(err, 'abort'); expect.type<'AbortError'>(err.name); // Narrows name } @@ -139,7 +139,7 @@ expect.type(Bounce.isTimeout(0)); expect.type<'TimeoutError'>(err.name); // Narrows name } - Bounce.ignore(err, 'timeout'); + Bounce.assert(err, 'timeout'); expect.type<'TimeoutError'>(err.name); // Narrows name } From 8b311a7db0ea6a3a6e439dfca1490445a9473b7e Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Sun, 31 Aug 2025 14:54:27 +0200 Subject: [PATCH 21/22] Make narrowing work when signal option is specified --- lib/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/index.d.ts b/lib/index.d.ts index 6f65944..1d0def0 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -60,7 +60,7 @@ export function rethrow(err: any, types: export function rethrow(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, decorate: D }): (T & D) | undefined; export function rethrow(err: any, types: TypeRule | TypeRule[], options: BounceOptions & { return: true, override: E }): E | undefined; export function rethrow(err: T, types: TypeRule | TypeRule[], options: BounceOptions & { return: true }): T | undefined; -export function rethrow(err: any, types: TypeRule | TypeRule[], options?: { strict: true }): asserts err is Error; +export function rethrow(err: any, types: TypeRule | TypeRule[], options?: BounceOptions & { strict?: true | undefined }): asserts err is Error; export function rethrow(err: any, types: TypeRule | TypeRule[], options?: BounceOptions): void; /** From e3aacc7294b61c0c83be419cb953b2860cb4a242 Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Mon, 1 Sep 2025 12:55:13 +0200 Subject: [PATCH 22/22] Throw TypeError on unhandled types --- lib/index.js | 39 +++++++++++++++++++++++++++------------ test/index.js | 9 +++++++++ 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/lib/index.js b/lib/index.js index 1ba7227..c6c6308 100755 --- a/lib/index.js +++ b/lib/index.js @@ -189,22 +189,37 @@ internals.rules = { internals.match = function (err, types) { - types = Array.isArray(types) ? types : [types]; + if (!Array.isArray(types)) { + return internals._match(err, types); + } + for (const type of types) { - if (typeof type === 'string') { - if (internals.rules[type](err)) { - return true; - } - } - else if (typeof type === 'object') { - if (Contain(err, type, { deep: true, part: true })) { - return true; - } - } - else if (err instanceof type) { + if (internals._match(err, type)) { return true; } } return false; }; + + +internals._match = function (err, type) { + + try { + switch (typeof type) { + case 'string': + return internals.rules[type](err); + + case 'object': + return Contain(err, type, { deep: true, part: true }); + + case 'function': + return err instanceof type; + } + } + catch (err) { + var error = err; + } + + throw new TypeError('Invalid "types" argument', error ? { reason: error } : undefined); +}; diff --git a/test/index.js b/test/index.js index 42a5f51..253d763 100755 --- a/test/index.js +++ b/test/index.js @@ -50,6 +50,15 @@ describe('Bounce', () => { expect(error).to.be.an.error(TypeError); }); + it('throws TypeError on unhandled types', () => { + + expect(() => Bounce.rethrow(new Error(), false)).to.throw(TypeError, 'Missing or invalid "types" argument'); + expect(() => Bounce.rethrow(new Error(), 'invalid')).to.throw(TypeError, 'Invalid "types" argument'); + expect(() => Bounce.rethrow(new Error(), ['invalid'])).to.throw(TypeError, 'Invalid "types" argument'); + expect(() => Bounce.rethrow(new Error(), 123)).to.throw(TypeError, 'Invalid "types" argument'); + expect(() => Bounce.rethrow(new Error(), () => 123)).to.throw(TypeError, 'Invalid "types" argument'); + }); + it('rethrows only system errors', () => { try {