diff --git a/lib/src/helpers/customError.ts b/lib/src/helpers/customError.ts index d5fd43e5..800c2003 100644 --- a/lib/src/helpers/customError.ts +++ b/lib/src/helpers/customError.ts @@ -6,7 +6,7 @@ * Licensed under the MIT license. */ -import { fnApply } from "../funcs/funcs"; +import { fnApply, fnCall } from "../funcs/funcs"; import { ArrSlice, CALL, CONSTRUCTOR, NAME, NULL_VALUE, PROTOTYPE } from "../internal/constants"; import { objCreate } from "../object/create"; import { objDefine } from "../object/define"; @@ -53,13 +53,16 @@ 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. 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. + * new Custom Error instance is being created. (Since v0.16.0) The callback is invoked with `this` bound to + * the same instance that is also passed as the `self` (1st) argument, so implementations may use either + * `this.prop = ...` or `self.prop = ...` interchangeably - this only applies when `constructCb` is a normal + * (non-arrow) function, as arrow functions ignore the bound `this` and keep their own lexically captured value. + * The 3rd argument, `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 @@ -157,20 +160,21 @@ function _setName(baseClass: any, name: string) { /*#__NO_SIDE_EFFECTS__*/ export function createCustomError( name: string, - constructCb?: ((self: any, args: IArguments, isOwnInstance?: boolean) => void) | null, + constructCb?: ((this: any, 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 _ctor(this: any) { + let ctorFn = function (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; + let isOwnInstance = _this[CONSTRUCTOR] === ctorFn; + try { safe(_setName, [theBaseClass, name]); let _self = fnApply(theBaseClass, _this, superArgsFn ? superArgsFn(theArgs) : ArrSlice[CALL](theArgs)) || _this; @@ -182,19 +186,28 @@ export function createCustomError(name, ctorFn, theBaseClass); } /** diff --git a/lib/test/src/common/helpers/throw.test.ts b/lib/test/src/common/helpers/throw.test.ts index 6ec0a224..c0754dcf 100644 --- a/lib/test/src/common/helpers/throw.test.ts +++ b/lib/test/src/common/helpers/throw.test.ts @@ -455,5 +455,57 @@ describe("throw helpers", () => { assert.deepEqual(baseCalls, [ true, false ], "Base sees true when it's the leaf, false when acting as a base class for Leaf"); }); }); + + describe("createCustomError constructCb this binding", () => { + it("binds this to self when instantiated directly", () => { + let seenThis: any; + let seenSelf: any; + let MyError = createCustomError("ConstructCbThisDirectTest", function (this: any, self) { + seenThis = this; + seenSelf = self; + }); + + let err = new (MyError as any)("boom"); + assert.ok(isError(err), "isError returns true"); + assert.equal(seenThis, seenSelf, "this === self inside constructCb"); + assert.equal(seenThis, err, "this === the constructed error instance"); + }); + + it("binds this to self at every level of a base-class chain", () => { + let calls: { name: string; thisIsSelf: boolean }[] = []; + + let Level1 = createCustomError("ConstructCbThisLevel1Test", function (this: any, self) { + calls.push({ name: "Level1", thisIsSelf: this === self }); + }); + let Level2 = createCustomError("ConstructCbThisLevel2Test", function (this: any, self) { + calls.push({ name: "Level2", thisIsSelf: this === self }); + }, Level1); + let Level3 = createCustomError("ConstructCbThisLevel3Test", function (this: any, self) { + calls.push({ name: "Level3", thisIsSelf: this === self }); + }, 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"); + calls.forEach((call) => { + assert.ok(call.thisIsSelf, call.name + ": this === self"); + }); + }); + + it("binds this to the same class's own leaf instance and to the subclass instance when used as a base", () => { + let baseThisIsSelf: boolean[] = []; + let Base = createCustomError("ConstructCbThisBaseTest", function (this: any, self) { + baseThisIsSelf.push(this === self); + }); + let Leaf = createCustomError("ConstructCbThisLeafTest", null, Base); + + let directErr = new (Base as any)("direct"); + let subclassErr = new (Leaf as any)("via-subclass"); + + assert.deepEqual(baseThisIsSelf, [ true, true ], "this === self for both the direct instance and when acting as a base for Leaf"); + assert.ok(isError(directErr) && isError(subclassErr), "both instances are errors"); + }); + }); });