Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 20 additions & 8 deletions lib/src/helpers/customError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -151,16 +157,20 @@ function _setName(baseClass: any, name: string) {
/*#__NO_SIDE_EFFECTS__*/
export function createCustomError<T extends ErrorConstructor = CustomErrorConstructor, B extends ErrorConstructor = ErrorConstructor>(
name: string,
constructCb?: ((self: any, args: IArguments) => void) | null,
constructCb?: ((self: any, args: IArguments, isOwnInstance?: boolean) => void) | null,
Comment thread
nev21 marked this conversation as resolved.
errorBase?: B,
superArgsFn?: ((args: IArguments) => ArrayLike<any>) | null): T {

let theBaseClass = errorBase || Error;
let orgName = theBaseClass[PROTOTYPE][NAME];
let captureFn = Error.captureStackTrace;
return _createCustomError<T>(name, function (this: any) {
return _createCustomError<T>(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;
Expand All @@ -172,12 +182,14 @@ export function createCustomError<T extends ErrorConstructor = CustomErrorConstr
}
}

// Make sure we only capture our stack details
captureFn && captureFn(_self, _this[CONSTRUCTOR]);

// Only capture the stack once, at the leaf-most class being instantiated - the
// constructorOpt target is always the same leaf class, so capturing at every level
// of a base class chain would just repeat the same work
isOwnInstance && captureFn && captureFn(_self, _this[CONSTRUCTOR]);

// Run the provided construction function
constructCb && constructCb(_self, theArgs);
constructCb && constructCb(_self, theArgs, isOwnInstance);

return _self;
} finally {
safe(_setName, [theBaseClass, orgName]);
Expand Down
75 changes: 75 additions & 0 deletions lib/test/src/common/helpers/throw.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
*/

import { assert } from "@nevware21/tripwire-chai";
import * as sinon from "sinon";
import { isError, objToString } from "../../../../src/helpers/base";
import { createCustomError, CustomErrorConstructor, throwUnsupported } from "../../../../src/helpers/customError";
import { dumpObj } from "../../../../src/helpers/diagnostics";
import { throwError, throwTypeError } from "../../../../src/helpers/throw";

const _hasCaptureStackTrace = !!(Error as any).captureStackTrace;


function _expectThrow<T extends Error = Error>(cb: () => never, message: string): T {
try {
Expand Down Expand Up @@ -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);
});
Comment thread
nev21 marked this conversation as resolved.

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);
Comment thread
nev21 marked this conversation as resolved.
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);
});
Comment thread
nev21 marked this conversation as resolved.
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");
});
});
});

Loading