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
38 changes: 35 additions & 3 deletions lib/src/helpers/customError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,12 @@ 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 Customer Error instance is being created.
* new Custom Error instance is being created.
* @param errorBase - [Optional] (since v0.9.6) The error class to extend for this class, defaults to Error.
* @param superArgsFn - [Optional] (since v0.12.7) An optional function that receives the constructor arguments and
* returns the arguments to pass to the base class constructor. When not provided all constructor
* arguments are forwarded to the base class. Use this to support a different argument order or
* to pass a subset of arguments to the base class (similar to calling `super(...)` in a class).
* @returns A new Error `class`
* @example
* ```ts
Expand Down Expand Up @@ -115,13 +119,41 @@ function _setName(baseClass: any, name: string) {
* theStartupError instanceof Error; // true
* theStartupError instanceof AppError; // true
* theStartupError instanceof StartupError; // true
*
* // ----------------------------------------------------------
* // Custom error with reordered / transformed arguments
* // (the superArgsFn maps constructor args to base class args)
* // ----------------------------------------------------------
*
* interface HttpErrorConstructor extends CustomErrorConstructor<HttpError> {
* new(statusCode: number, message: string): HttpError;
* (statusCode: number, message: string): HttpError;
* }
*
* interface HttpError extends Error {
* readonly statusCode: number;
* }
*
* // HttpError takes (statusCode, message) but base Error expects (message)
* let MyHttpError = createCustomError<HttpErrorConstructor>("HttpError",
* (self, args) => {
* self.statusCode = args[0];
* },
* Error,
* (args) => [ args[1] ] // pass only the message to base Error constructor
* );
*
* let err = new MyHttpError(404, "Not Found");
* err.message; // "Not Found"
* err.statusCode; // 404
* ```
*/
/*#__NO_SIDE_EFFECTS__*/
export function createCustomError<T extends ErrorConstructor = CustomErrorConstructor, B extends ErrorConstructor = ErrorConstructor>(
name: string,
constructCb?: ((self: any, args: IArguments) => void) | null,
errorBase?: B): T {
errorBase?: B,
superArgsFn?: ((args: IArguments) => ArrayLike<any>) | null): T {

let theBaseClass = errorBase || Error;
let orgName = theBaseClass[PROTOTYPE][NAME];
Expand All @@ -131,7 +163,7 @@ export function createCustomError<T extends ErrorConstructor = CustomErrorConstr
let theArgs = arguments;
try {
safe(_setName, [theBaseClass, name]);
let _self = fnApply(theBaseClass, _this, ArrSlice[CALL](theArgs)) || _this;
let _self = fnApply(theBaseClass, _this, superArgsFn ? superArgsFn(theArgs) : ArrSlice[CALL](theArgs)) || _this;
if (_self !== _this) {
// Looks like runtime error constructor reset the prototype chain, so restore it
let orgProto = objGetPrototypeOf(_this);
Expand Down
117 changes: 117 additions & 0 deletions lib/test/src/common/helpers/throw.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,5 +263,122 @@ describe("throw helpers", () => {
assert.ok(theError instanceof ApplicationError, "Check that the startupError is an ApplicationError");
});
});

describe("createCustomError with superArgsFn", () => {
it("reorders arguments before passing to base class", () => {
interface HttpErrorConstructor extends CustomErrorConstructor<HttpError> {
new(statusCode: number, message: string): HttpError;
(statusCode: number, message: string): HttpError;
}

interface HttpError extends Error {
readonly statusCode: number;
}

let MyHttpError = createCustomError<HttpErrorConstructor>("HttpError",
(self, args) => {
self.statusCode = args[0];
},
Error,
(args) => [ args[1] ] // pass only the message to base Error constructor
);

let err = _expectThrow<HttpError>(() => {
throw new MyHttpError(404, "Not Found");
}, "Not Found");

assert.ok(err instanceof Error, "The custom error is an Error");
assert.ok(isError(err), "isError returns true");
assert.equal(err.name, "HttpError", "Name is HttpError");
assert.equal(err.message, "Not Found", "Message is set from second arg");
assert.equal(err.statusCode, 404, "statusCode is set from first arg");
});

it("passes subset of arguments to base class", () => {
interface DetailedErrorConstructor extends CustomErrorConstructor<DetailedError> {
new(message: string, code: number, detail: string): DetailedError;
(message: string, code: number, detail: string): DetailedError;
}

interface DetailedError extends Error {
readonly code: number;
readonly detail: string;
}

let MyDetailedError = createCustomError<DetailedErrorConstructor>("DetailedError",
(self, args) => {
self.code = args[1];
self.detail = args[2];
},
Error,
(args) => [ args[0] ] // pass only message to base Error
);

let err = _expectThrow<DetailedError>(() => {
throw new MyDetailedError("Something failed", 42, "extra detail");
}, "Something failed");

assert.ok(err instanceof Error, "The custom error is an Error");
assert.ok(isError(err), "isError returns true");
assert.equal(err.name, "DetailedError", "Name is DetailedError");
assert.equal(err.message, "Something failed", "Message is set correctly");
assert.equal(err.code, 42, "code is set correctly");
assert.equal(err.detail, "extra detail", "detail is set correctly");
});

it("works with custom error base class", () => {
interface AppErrorConstructor extends CustomErrorConstructor<AppError> {
new(message: string): AppError;
(message: string): AppError;
}
interface AppError extends Error {}

interface ServiceErrorConstructor extends CustomErrorConstructor<ServiceError> {
new(service: string, message: string): ServiceError;
(service: string, message: string): ServiceError;
}
interface ServiceError extends AppError {
readonly service: string;
}

let AppErrorCls = createCustomError<AppErrorConstructor>("AppError");
let ServiceErrorCls = createCustomError<ServiceErrorConstructor>("ServiceError",
(self, args) => {
self.service = args[0];
},
AppErrorCls,
(args) => [ args[1] ] // pass message to AppError base class
);

let err = _expectThrow<ServiceError>(() => {
throw new ServiceErrorCls("auth-service", "Unauthorized");
}, "Unauthorized");

assert.ok(err instanceof Error, "is an Error");
assert.ok(err instanceof AppErrorCls, "is an AppError");
assert.ok(err instanceof ServiceErrorCls, "is a ServiceError");
assert.ok(isError(err), "isError returns true");
assert.equal(err.name, "ServiceError", "Name is ServiceError");
assert.equal(err.message, "Unauthorized", "Message is set from second arg");
assert.equal(err.service, "auth-service", "service is set from first arg");
});

it("null superArgsFn behaves like no superArgsFn (passes all args)", () => {
interface MyErrorConstructor extends CustomErrorConstructor<MyError> {
new(message: string): MyError;
(message: string): MyError;
}
interface MyError extends Error {}

let MyErrorCls = createCustomError<MyErrorConstructor>("MyNullSuperError", null, Error, null);

let err = _expectThrow<MyError>(() => {
throw new MyErrorCls("hello");
}, "hello");

assert.ok(isError(err), "isError returns true");
assert.equal(err.message, "hello", "Message is set correctly");
});
});
});