From d016febbdd616141f4767575c652e22911269b53 Mon Sep 17 00:00:00 2001 From: Nev Date: Fri, 17 Jul 2026 21:49:38 -0700 Subject: [PATCH] feat(errors): expose isOwnInstance to createCustomError's constructCb and skip redundant stack captures Custom errors built with createCustomError can extend other custom errors, chaining constructCb calls at every level. There was previously no way for a level to know whether it was the leaf class actually being instantiated or just running as a base-class step for some subclass further down the chain, so hierarchies with their own per-level stack capture logic (e.g. a TripwireError -> AssertionError -> AssertionFailure -> AssertionFatal chain) had no way to skip that work except at the leaf. - constructCb now receives a 3rd argument, isOwnInstance, computed by comparing the instance's resolved constructor against the class's own constructor function via a self-referencing named function expression The internal automatic Error.captureStackTrace call is now gated by the same isOwnInstance check, so it only fires once per instance (at the leaf-most class) instead of redundantly at every level of a chain - Add regression tests covering the internal capture optimization and isOwnInstance correctness across single classes, multi-level chains, and a class used both directly and as a base --- CHANGELOG.md | 5 ++ lib/src/helpers/customError.ts | 28 ++++++--- lib/test/src/common/helpers/throw.test.ts | 75 +++++++++++++++++++++++ 3 files changed, 100 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66ee5a9e..3bb6b7a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ ### Features +- feat(errors): expose `isOwnInstance` to `createCustomError`'s `constructCb` and skip redundant stack captures + - `constructCb` now receives a 3rd argument, `isOwnInstance`, which is `true` when the class is the leaf-most type actually being instantiated and `false` when its constructor is only running as a base-class step for a subclass further down an inheritance chain + - The internal `Error.captureStackTrace` call is now gated by the same check, so it only fires once per instance (at the leaf-most class) instead of redundantly at every level of a chain + - Lets custom error hierarchies with their own per-level stack-capture logic skip that work except at the leaf + ### Bug Fixes # v0.15.0 May 29th, 2026 diff --git a/lib/src/helpers/customError.ts b/lib/src/helpers/customError.ts index b9ef350f..d5fd43e5 100644 --- a/lib/src/helpers/customError.ts +++ b/lib/src/helpers/customError.ts @@ -53,7 +53,13 @@ function _setName(baseClass: any, name: string) { * @group Error * @param name - The name of the Custom Error * @param constructCb - [Optional] An optional callback function to call when a - * new Custom Error instance is being created. + * new Custom Error instance is being created. The 3rd argument (since v0.16.0), `isOwnInstance`, is `true` + * when this class is the leaf-most (most-derived) type actually being instantiated (eg. `new ThisClass()`), + * and `false` when this class's own constructor is only running because it is a base class of some other + * custom error further down an inheritance chain (eg. `new SomeSubClass()`, where `SomeSubClass` was created + * via `createCustomError(..., ThisClass)`). Use this to skip work - such as your own stack-trace capture - + * that only the leaf-most instance actually needs, since a base class's own construction step runs (and + * would otherwise redo that work) for every instance of every one of its subclasses too. * @param errorBase - [Optional] (since v0.9.6) The error class to extend for this class, defaults to Error. * @param superArgsFn - [Optional] (since v0.13.0) An optional function that receives the constructor arguments and * returns the arguments to pass to the base class constructor. When not provided all constructor @@ -151,16 +157,20 @@ function _setName(baseClass: any, name: string) { /*#__NO_SIDE_EFFECTS__*/ export function createCustomError( name: string, - constructCb?: ((self: any, args: IArguments) => void) | null, + constructCb?: ((self: any, args: IArguments, isOwnInstance?: boolean) => void) | null, errorBase?: B, superArgsFn?: ((args: IArguments) => ArrayLike) | null): T { let theBaseClass = errorBase || Error; let orgName = theBaseClass[PROTOTYPE][NAME]; let captureFn = Error.captureStackTrace; - return _createCustomError(name, function (this: any) { + return _createCustomError(name, function _ctor(this: any) { let _this = this; let theArgs = arguments; + // this[CONSTRUCTOR] always resolves (via the prototype chain) to the leaf-most class + // actually being instantiated, so comparing it against ourselves tells us whether we're + // that leaf class or just being invoked as a base class for a child further down the chain + let isOwnInstance = _this[CONSTRUCTOR] === _ctor; try { safe(_setName, [theBaseClass, name]); let _self = fnApply(theBaseClass, _this, superArgsFn ? superArgsFn(theArgs) : ArrSlice[CALL](theArgs)) || _this; @@ -172,12 +182,14 @@ export function createCustomError(cb: () => never, message: string): T { try { @@ -380,5 +383,77 @@ describe("throw helpers", () => { assert.equal(err.message, "hello", "Message is set correctly"); }); }); + + describe("createCustomError internal stack capture optimization", () => { + it("captures the stack only once for a multi-level chain", () => { + if (!_hasCaptureStackTrace) { + return; + } + + let captureSpy = sinon.spy(Error as any, "captureStackTrace"); + try { + let Level1 = createCustomError("Level1CaptureTest"); + let Level2 = createCustomError("Level2CaptureTest", null, Level1); + let Level3 = createCustomError("Level3CaptureTest", null, Level2); + + captureSpy.resetHistory(); + let err = new (Level3 as any)("boom"); + assert.ok(isError(err), "isError returns true"); + assert.equal(captureSpy.callCount, 1, "Expected exactly one automatic capture, for the leaf-most class"); + } finally { + captureSpy.restore(); + } + }); + }); + + describe("createCustomError constructCb isOwnInstance argument", () => { + it("is true when the class is instantiated directly with no base chain", () => { + let seen: (boolean | undefined)[] = []; + let MyError = createCustomError("IsOwnInstanceDirectTest", (self, args, isOwnInstance) => { + seen.push(isOwnInstance); + }); + + new (MyError as any)("boom"); + assert.deepEqual(seen, [true], "constructCb should see isOwnInstance === true"); + }); + + it("is false for base classes and true only for the leaf-most class in a chain", () => { + let calls: { name: string; isOwnInstance: boolean | undefined }[] = []; + + let Level1 = createCustomError("IsOwnInstanceLevel1Test", (self, args, isOwnInstance) => { + calls.push({ name: "Level1", isOwnInstance }); + }); + let Level2 = createCustomError("IsOwnInstanceLevel2Test", (self, args, isOwnInstance) => { + calls.push({ name: "Level2", isOwnInstance }); + }, Level1); + let Level3 = createCustomError("IsOwnInstanceLevel3Test", (self, args, isOwnInstance) => { + calls.push({ name: "Level3", isOwnInstance }); + }, Level2); + + let err = new (Level3 as any)("boom"); + assert.ok(isError(err), "isError returns true"); + + assert.equal(calls.length, 3, "Every level's constructCb should still run"); + assert.equal(calls[0].name, "Level1", "Level1's own constructCb runs first (innermost base call)"); + assert.equal(calls[0].isOwnInstance, false, "Level1 is only running as a base class here"); + assert.equal(calls[1].name, "Level2"); + assert.equal(calls[1].isOwnInstance, false, "Level2 is only running as a base class here"); + assert.equal(calls[2].name, "Level3"); + assert.equal(calls[2].isOwnInstance, true, "Level3 is the leaf-most class actually being instantiated"); + }); + + it("the same base class sees isOwnInstance true when instantiated directly and false when used as a base", () => { + let baseCalls: (boolean | undefined)[] = []; + let Base = createCustomError("IsOwnInstanceBaseTest", (self, args, isOwnInstance) => { + baseCalls.push(isOwnInstance); + }); + let Leaf = createCustomError("IsOwnInstanceLeafTest", null, Base); + + new (Base as any)("direct"); + new (Leaf as any)("via-subclass"); + + assert.deepEqual(baseCalls, [ true, false ], "Base sees true when it's the leaf, false when acting as a base class for Leaf"); + }); + }); });