From b3a508c9a080e00a5e39ffa352a38e785b8cea9c Mon Sep 17 00:00:00 2001 From: Michael Courtin Date: Thu, 30 Dec 2021 16:35:58 +0100 Subject: [PATCH] feat(cactus-common): add createRuntimeErrorWithCause() & newRex() Utility functions to conveniently re-throw excpetions typed as unknown by their catch block (which is the default since Typescript v4.4). Example usage can and much more documentation can be seen here: `packages/cactus-common/src/main/typescript/exception/create-runtime-error-with-cause.ts` and here `packages/cactus-common/src/test/typescript/unit/exception/create-runtime-error-with-cause.test.ts` Co-authored-by: Peter Somogyvari Closes: #1702 [skip ci] Signed-off-by: Michael Courtin Signed-off-by: Peter Somogyvari --- .../src/main/typescript/api-server.ts | 69 ++-- packages/cactus-common/package.json | 5 + .../exception/coerce-unknown-to-error.ts | 46 +++ .../create-runtime-error-with-cause.ts | 104 +++++ .../typescript/exception/error-from-symbol.ts | 1 + .../exception/error-from-unknown-throwable.ts | 18 + .../src/main/typescript/public-api.ts | 6 + .../src/main/typescript/types/has-key.ts | 6 + .../create-runtime-error-with-cause.test.ts | 383 ++++++++++++++++++ 9 files changed, 600 insertions(+), 38 deletions(-) create mode 100644 packages/cactus-common/src/main/typescript/exception/coerce-unknown-to-error.ts create mode 100644 packages/cactus-common/src/main/typescript/exception/create-runtime-error-with-cause.ts create mode 100644 packages/cactus-common/src/main/typescript/exception/error-from-symbol.ts create mode 100644 packages/cactus-common/src/main/typescript/exception/error-from-unknown-throwable.ts create mode 100644 packages/cactus-common/src/main/typescript/types/has-key.ts create mode 100644 packages/cactus-common/src/test/typescript/unit/exception/create-runtime-error-with-cause.test.ts diff --git a/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts b/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts index 056d0a05ae..64e5800af9 100644 --- a/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts @@ -48,6 +48,7 @@ import { Bools, Logger, LoggerProvider, + newRex, Servers, } from "@hyperledger/cactus-common"; @@ -248,17 +249,17 @@ export class ApiServer { } return { addressInfoCockpit, addressInfoApi, addressInfoGrpc }; - } catch (ex) { - const errorMessage = `Failed to start ApiServer: ${ex.stack}`; - this.log.error(errorMessage); + } catch (ex1: unknown) { + const context = "Failed to start ApiServer"; + this.log.error(context, ex1); this.log.error(`Attempting shutdown...`); try { await this.shutdown(); this.log.info(`Server shut down after crash OK`); - } catch (ex) { - this.log.error(ApiServer.E_POST_CRASH_SHUTDOWN, ex); + } catch (ex2: unknown) { + this.log.error(ApiServer.E_POST_CRASH_SHUTDOWN, ex2); } - throw new Error(errorMessage); + throw newRex(context, ex1); } } @@ -304,11 +305,11 @@ export class ApiServer { await this.getPluginImportsCount(), ); return this.pluginRegistry; - } catch (e) { + } catch (ex: unknown) { this.pluginRegistry = new PluginRegistry({ plugins: [] }); - const errorMessage = `Failed init PluginRegistry: ${e.stack}`; - this.log.error(errorMessage); - throw new Error(errorMessage); + const context = "Failed to init PluginRegistry"; + this.log.debug(context, ex); + throw newRex(context, ex); } } @@ -368,15 +369,10 @@ export class ApiServer { await plugin.onPluginInit(); return plugin; - } catch (error) { - const errorMessage = `${fnTag} failed instantiating plugin '${packageName}' with the instanceId '${options.instanceId}'`; - this.log.error(errorMessage, error); - - if (error instanceof Error) { - throw new RuntimeError(errorMessage, error); - } else { - throw new RuntimeError(errorMessage, JSON.stringify(error)); - } + } catch (ex: unknown) { + const context = `${fnTag} failed instantiating plugin '${packageName}' with the instanceId '${options.instanceId}'`; + this.log.debug(context, ex); + throw newRex(context, ex); } } @@ -397,10 +393,10 @@ export class ApiServer { try { await fs.mkdirp(pluginPackageDir); this.log.debug(`${pkgName} plugin package dir: %o`, pluginPackageDir); - } catch (ex) { - const errorMessage = + } catch (ex: unknown) { + const context = "Could not create plugin installation directory, check the file-system permissions."; - throw new RuntimeError(errorMessage, ex); + throw newRex(context, ex); } try { lmify.setPackageManager("npm"); @@ -418,19 +414,15 @@ export class ApiServer { // "--ignore-workspace-root-check", ]); this.log.debug("%o install result: %o", pkgName, out); - if (out.exitCode !== 0) { - throw new RuntimeError("Non-zero exit code: ", JSON.stringify(out)); + if (out?.exitCode && out.exitCode !== 0) { + const eMsg = "Non-zero exit code returned by lmify.install() indicating that the underlying npm install OS process had encountered a problem:"; + throw newRex(eMsg, out); } this.log.info(`Installed ${pkgName} OK`); - } catch (ex) { - const errorMessage = `${fnTag} failed installing plugin '${pkgName}`; - this.log.error(errorMessage, ex); - - if (ex instanceof Error) { - throw new RuntimeError(errorMessage, ex); - } else { - throw new RuntimeError(errorMessage, JSON.stringify(ex)); - } + } catch (ex: unknown) { + const context = `${fnTag} failed installing plugin '${pkgName}`; + this.log.debug(ex, context); + throw newRex(context, ex); } } @@ -451,24 +443,25 @@ export class ApiServer { this.log.info(`Stopped ${webServicesShutdown.length} WS plugin(s) OK`); if (this.httpServerApi?.listening) { - this.log.info(`Closing HTTP server of the API...`); + this.log.info(`Closing Cacti HTTP server of the API...`); await Servers.shutdown(this.httpServerApi); this.log.info(`Close HTTP server of the API OK`); } if (this.httpServerCockpit?.listening) { - this.log.info(`Closing HTTP server of the cockpit ...`); + this.log.info(`Closing Cacti HTTP server of the cockpit ...`); await Servers.shutdown(this.httpServerCockpit); this.log.info(`Close HTTP server of the cockpit OK`); } if (this.grpcServer) { - this.log.info(`Closing gRPC server ...`); + this.log.info(`Closing Cacti gRPC server ...`); await new Promise((resolve, reject) => { this.grpcServer.tryShutdown((ex?: Error) => { if (ex) { - this.log.error("Failed to shut down gRPC server: ", ex); - reject(ex); + const eMsg = "Failed to shut down gRPC server of the Cacti API server."; + this.log.debug(eMsg, ex); + reject(newRex(eMsg, ex)); } else { resolve(); } diff --git a/packages/cactus-common/package.json b/packages/cactus-common/package.json index 03fb263249..cb9b7e0bad 100644 --- a/packages/cactus-common/package.json +++ b/packages/cactus-common/package.json @@ -33,6 +33,11 @@ "name": "Peter Somogyvari", "email": "peter.somogyvari@accenture.com", "url": "https://accenture.com" + }, + { + "name": "Michael Courtin", + "email": "michael.courtin@accenture.com", + "url": "https://accenture.com" } ], "main": "dist/lib/main/typescript/index.js", diff --git a/packages/cactus-common/src/main/typescript/exception/coerce-unknown-to-error.ts b/packages/cactus-common/src/main/typescript/exception/coerce-unknown-to-error.ts new file mode 100644 index 0000000000..b5b22ee180 --- /dev/null +++ b/packages/cactus-common/src/main/typescript/exception/coerce-unknown-to-error.ts @@ -0,0 +1,46 @@ +import stringify from "fast-safe-stringify"; +import { ErrorFromUnknownThrowable } from "./error-from-unknown-throwable"; +import { ErrorFromSymbol } from "./error-from-symbol"; + +/** + * Safely converts `unknown` to an `Error` with doing a best effort to ensure + * that root cause analysis information is not lost. The idea here is to help + * people who are reading logs of errors while trying to figure out what went + * wrong after a crash. + * + * Often in Javascript this is much harder than it could be due to lack of + * runtime checks by the JSVM (Javascript Virtual Machine) on the values/objects + * that are being thrown. + * + * @param x The value/object whose type information is completely unknown at + * compile time, such as the input parameter of a catch block (which could + * be anything because the JS runtime has no enforcement on it at all, e.g. + * you can throw null, undefined, empty strings of whatever else you'd like.) + * @returns An `Error` object that is the original `x` if it was an `Error` + * instance to begin with or a stringified JSON representation of `x` otherwise. + */ +export function coerceUnknownToError(x: unknown): Error { + if (typeof x === "symbol") { + const symbolAsStr = x.toString(); + return new ErrorFromSymbol(symbolAsStr); + } else if (x instanceof Error) { + return x; + } else { + const xAsJson = stringify(x, (_, value) => + typeof value === "bigint" ? value.toString() + "n" : value, + ); + return new ErrorFromUnknownThrowable(xAsJson); + } +} + +/** + * This is an alias to `coerceUnknownToError(x: unknown)`. + * + * The shorter name allows for different style choices to be made by the person + * writing the error handling code. + * + * @see #coerceUnknownToError + */ +export function asError(x: unknown): Error { + return coerceUnknownToError(x); +} diff --git a/packages/cactus-common/src/main/typescript/exception/create-runtime-error-with-cause.ts b/packages/cactus-common/src/main/typescript/exception/create-runtime-error-with-cause.ts new file mode 100644 index 0000000000..b951d5d187 --- /dev/null +++ b/packages/cactus-common/src/main/typescript/exception/create-runtime-error-with-cause.ts @@ -0,0 +1,104 @@ +import { RuntimeError } from "run-time-error"; +import { coerceUnknownToError } from "./coerce-unknown-to-error"; + +/** + * ### STANDARD EXCEPTION HANDLING - EXAMPLE WITH RE-THROW: + * + * Use the this utility function and pass in any throwable of whatever type and format + * The underlying implementation will take care of determining if it's a valid + * `Error` instance or not and act accordingly with avoding information loss + * being the number one priority. + * + * You can perform a fast-fail re-throw with additional context like the snippet + * below. + * Notice that we log on the debug level inside the catch block to make sure that + * if somebody higher up in the callstack ends up handling this exception then + * it will never get logged on the error level which is good because if it did + * that would be a false-positive, annoying system administrators who have to + * figure out which errors in their production logs need to be ignored and which + * ones are legitimate. + * The trade-off with the above is trust: Specifically, we are trusting the + * person above us in the callstack to either correctly handle the exception + * or make sure that it does get logged on the error level. If they fail to do + * either one of those, then we'll have silent failures on our hand that will + * be hard to debug. + * Lack of the above kind of trust is usually what pushes people to just go for + * it and log their caught exceptions on the error level but this most likely + * a mistake in library code where there just isn't enough context to know if + * an error is legitimate or not most of the time. If you are writing application + * logic then it's usually a simpler decision with more information at your + * disposal. + * + * The underlying concept is that if you log something on an error level, you + * indicate that another human should fix a bug that is in the code. E.g., + * when they see the error logs, they should go and fix something. + * + * ```typescript + * public doSomething(): void { + * try { + * someSubTaskToExecute(); + * } catch (ex) { + * const eMsg = "Failed to run **someSubTask** while doing **something**:" + * this.log.debug(eMsg, ex); + * throw createRuntimeErrorWithCause(eMsg, ex); + * } + * ``` + * + * ### EXCEPTION HANDLING WITH CONDITIONAL HANDLING AND RE-THROW - EXAMPLE: + * + * In case you need to do a conditional exception-handling: + * - Use the RuntimeError to re-throw and + * provide the previous exception as cause in the new RuntimeError to retain + * the information and distinguish between an exception you can handle and + * recover from and one you can't + * + * ```typescript + * public async doSomething(): Promise { + * try { + * await doSubTaskThatsAPartOfDoingSomething(); + * } catch (ex) { + * if (ex instanceof MyErrorThatICanHandleAndRecoverFrom) { + * // An exception with a fixable scenario we can recover from thru an additional handling + * // do something here to handle and fix the issue + * // where "fixing" means that the we end up recovering + * // OK instead of having to crash. Recovery means that + * // we are confident that the second sub-task is safe to proceed with + * // despite of the error that was caught here + * this.log.debug("We've got an failure in 'doSubTaskThatsAPartOfDoingSomething()' but we could fix it and recover to continue".); + * } else { + * // An "unexpected exception" where we want to fail immediately + * // to avoid follow-up problems + * const context = "We got an severe failure in 'doSubTaskThatsAPartOfDoingSomething()' and need to stop directly here to avoid follow-up problems"; + * this.log.erorr(context, ex); + * throw newRex(context, ex); + * } + * } + * const result = await doSecondAndFinalSubTask(); + * return result; // 42 + * } + * ``` + * + * @param message The contextual information that will be passed into the + * constructor of the returned {@link RuntimeError} instance. + * @param cause The caught throwable which we do not know the exact type of but + * need to make sure that whatever information is in t here is not lost. + * @returns The instance that has the combined information of the input parameters. + */ +export function createRuntimeErrorWithCause( + message: string, + cause: unknown, +): RuntimeError { + const innerEx = coerceUnknownToError(cause); + return new RuntimeError(message, innerEx); +} + +/** + * An alias to the `createRuntimeErrorWithCause` function for those prefering + * a shorter utility for their personal style. + * + * @see {@link createRuntimeErrorWithCause} + * @returns `RuntimeError` + */ +export function newRex(message: string, cause: unknown): RuntimeError { + return createRuntimeErrorWithCause(message, cause); +} diff --git a/packages/cactus-common/src/main/typescript/exception/error-from-symbol.ts b/packages/cactus-common/src/main/typescript/exception/error-from-symbol.ts new file mode 100644 index 0000000000..8adea94e5d --- /dev/null +++ b/packages/cactus-common/src/main/typescript/exception/error-from-symbol.ts @@ -0,0 +1 @@ +export class ErrorFromSymbol extends Error {} diff --git a/packages/cactus-common/src/main/typescript/exception/error-from-unknown-throwable.ts b/packages/cactus-common/src/main/typescript/exception/error-from-unknown-throwable.ts new file mode 100644 index 0000000000..592c57083b --- /dev/null +++ b/packages/cactus-common/src/main/typescript/exception/error-from-unknown-throwable.ts @@ -0,0 +1,18 @@ +/** + * A custom `Error` class designed to encode information about the origin of + * the information contained inside. + * + * Specifically this class is to be used when a catch block has encountered a + * throwable [1] that was not an instance of `Error`. + * + * This should help people understand the contents a little more while searching + * for the root cause of a crash (by letting them know that we had encoutnered + * a non-Error catch block parameter and we wrapped it in this `Error` sub-class + * purposefully to make it easier to deal with it) + * + * [1]: A throwable is a value or object that is possible to be thrown in the + * place of an `Error` object. This - as per the rules of Javascript - can be + * literally anything, NaN, undefined, null, etc. + */ +export class ErrorFromUnknownThrowable extends Error { +} diff --git a/packages/cactus-common/src/main/typescript/public-api.ts b/packages/cactus-common/src/main/typescript/public-api.ts index 379220a4b5..893eb5f473 100755 --- a/packages/cactus-common/src/main/typescript/public-api.ts +++ b/packages/cactus-common/src/main/typescript/public-api.ts @@ -27,3 +27,9 @@ export { } from "./authzn/i-jose-fitting-jwt-params"; export { isRecord } from "./types/is-record"; +export { hasKey } from "./types/has-key"; + +export { asError, coerceUnknownToError } from "./exception/coerce-unknown-to-error"; +export { createRuntimeErrorWithCause, newRex } from "./exception/create-runtime-error-with-cause"; +export { ErrorFromUnknownThrowable } from "./exception/error-from-unknown-throwable"; +export { ErrorFromSymbol } from "./exception/error-from-symbol"; diff --git a/packages/cactus-common/src/main/typescript/types/has-key.ts b/packages/cactus-common/src/main/typescript/types/has-key.ts new file mode 100644 index 0000000000..affa946dc1 --- /dev/null +++ b/packages/cactus-common/src/main/typescript/types/has-key.ts @@ -0,0 +1,6 @@ +export function hasKey( + x: unknown, + key: T, +): x is { [key in T]: unknown } { + return Boolean(typeof x === "object" && x && key in x); +} diff --git a/packages/cactus-common/src/test/typescript/unit/exception/create-runtime-error-with-cause.test.ts b/packages/cactus-common/src/test/typescript/unit/exception/create-runtime-error-with-cause.test.ts new file mode 100644 index 0000000000..7ec1588fa8 --- /dev/null +++ b/packages/cactus-common/src/test/typescript/unit/exception/create-runtime-error-with-cause.test.ts @@ -0,0 +1,383 @@ +import { v4 as uuidV4 } from "uuid"; +import "jest-extended"; + +import { createRuntimeErrorWithCause } from "../../../../main/typescript/public-api"; +import stringify from "fast-safe-stringify"; +import { RuntimeError } from "run-time-error"; + +describe("createRuntimeErrorWithCause() & newRex()", () => { + it("avoids losing information of inner exception: RuntimeError instance", () => { + const aCauseMessage = uuidV4(); + const eMsg = uuidV4(); + + const anError = new RuntimeError(aCauseMessage); + + try { + throw anError; + } catch (ex: unknown) { + const rex = createRuntimeErrorWithCause(eMsg, ex); + const { cause, message, name, stack } = rex; + expect(cause).toBeInstanceOf(Error); + expect((cause as Error).message).toContain(aCauseMessage); + expect(name).toBe("RuntimeError"); + expect(message).toBeString(); + expect(message).toEqual(eMsg); + expect(stack).toContain(eMsg); + + const rexAsJson = stringify(rex); + expect(rexAsJson).toContain(eMsg); + expect(rexAsJson).toContain(aCauseMessage); + } + }); + + it("avoids losing information of inner exception: nexted RuntimeError instances", () => { + const aCauseMessage = uuidV4(); + const eMsg = uuidV4(); + const innerEMsg1 = uuidV4(); + const innerEMsg2 = uuidV4(); + + const innerEx1 = new RuntimeError(innerEMsg1); + const innerEx2 = new RuntimeError(innerEMsg2, innerEx1); + + const anError = new RuntimeError(aCauseMessage, innerEx2); + + try { + throw anError; + } catch (ex: unknown) { + const rex = createRuntimeErrorWithCause(eMsg, ex); + const { cause, message, name, stack } = rex; + expect(cause).toBeInstanceOf(Error); + expect((cause as Error).message).toContain(aCauseMessage); + expect(name).toBe("RuntimeError"); + expect(message).toBeString(); + expect(message).toEqual(eMsg); + expect(stack).toContain(eMsg); + + const rexAsJson = stringify(rex); + expect(rexAsJson).toContain(eMsg); + expect(rexAsJson).toContain(aCauseMessage); + expect(rexAsJson).toContain(innerEMsg1); + expect(rexAsJson).toContain(innerEMsg2); + } + }); + + it("avoids losing information of inner exception: Error instance", () => { + const aCauseMessage = uuidV4(); + const eMsg = uuidV4(); + + const anError = new Error(aCauseMessage); + + try { + throw anError; + } catch (ex: unknown) { + const rex = createRuntimeErrorWithCause(eMsg, ex); + const { cause, message, name, stack } = rex; + expect(cause).toBeInstanceOf(Error); + expect((cause as Error).message).toContain(aCauseMessage); + expect(name).toBe("RuntimeError"); + expect(message).toBeString(); + expect(message).toEqual(eMsg); + expect(stack).toContain(eMsg); + + const rexAsJson = stringify(rex); + expect(rexAsJson).toContain(eMsg); + expect(rexAsJson).toContain(aCauseMessage); + } + }); + + it("avoids losing information of inner exception: Error shaped POJO", () => { + const aCauseMessage = uuidV4(); + const aStack = uuidV4(); + const eMsg = uuidV4(); + + const fakeErrorWithStack = { + message: aCauseMessage, + stack: aStack, + }; + + try { + throw fakeErrorWithStack; + } catch (ex: unknown) { + const rex = createRuntimeErrorWithCause(eMsg, ex); + const { cause, message, name, stack } = rex; + expect(cause).toBeInstanceOf(Error); + expect((cause as Error).message).toContain(aCauseMessage); + expect((cause as Error).stack).toContain(aStack); + expect(name).toBe("RuntimeError"); + expect(message).toBeString(); + expect(message).toEqual(eMsg); + expect(stack).toContain(eMsg); + + const rexAsJson = stringify(rex); + expect(rexAsJson).toContain(eMsg); + expect(rexAsJson).toContain(aCauseMessage); + expect(rexAsJson).toContain(aStack); + } + }); + + it("avoids losing information of inner exception: Error shaped circular POJO", () => { + const aCauseMessage = uuidV4(); + const aStack = uuidV4(); + const eMsg = uuidV4(); + + const fakeErrorWithStack = { + message: aCauseMessage, + stack: aStack, + circularPropertyReference: {}, + }; + fakeErrorWithStack.circularPropertyReference = fakeErrorWithStack; + + try { + throw fakeErrorWithStack; + } catch (ex: unknown) { + const rex = createRuntimeErrorWithCause(eMsg, ex); + const { cause, message, name, stack } = rex; + expect(cause).toBeInstanceOf(Error); + expect((cause as Error).message).toContain(aCauseMessage); + expect((cause as Error).stack).toContain(aStack); + expect(name).toBe("RuntimeError"); + expect(message).toBeString(); + expect(message).toEqual(eMsg); + expect(stack).toContain(eMsg); + + const rexAsJson = stringify(rex); + expect(rexAsJson).toContain(eMsg); + expect(rexAsJson).toContain(aCauseMessage); + expect(rexAsJson).toContain(aStack); + } + }); + + it("avoids losing information of inner exception: undefined", () => { + const eMsg = uuidV4(); + + try { + throw undefined; + } catch (ex: unknown) { + const rex = createRuntimeErrorWithCause(eMsg, ex); + const { cause, message, name, stack } = rex; + expect(cause).toBeInstanceOf(Error); + expect(name).toBe("RuntimeError"); + expect(message).toBeString(); + expect(message).toEqual(eMsg); + expect(stack).toContain(eMsg); + expect(cause?.constructor.name).toEqual("ErrorFromUnknownThrowable"); + + const rexAsJson = stringify(rex); + expect(rexAsJson).toContain(eMsg); + } + }); + + it("avoids losing information of inner exception: null", () => { + const eMsg = uuidV4(); + + try { + throw null; + } catch (ex: unknown) { + const rex = createRuntimeErrorWithCause(eMsg, ex); + const { cause, message, name, stack } = rex; + expect(cause).toBeInstanceOf(Error); + expect(name).toBe("RuntimeError"); + expect(message).toBeString(); + expect(message).toEqual(eMsg); + expect(stack).toContain(eMsg); + expect(cause?.constructor.name).toEqual("ErrorFromUnknownThrowable"); + + const rexAsJson = stringify(rex); + expect(rexAsJson).toContain(eMsg); + } + }); + + it("avoids losing information of inner exception: NaN", () => { + const eMsg = uuidV4(); + + try { + throw NaN; + } catch (ex: unknown) { + const rex = createRuntimeErrorWithCause(eMsg, ex); + const { cause, message, name, stack } = rex; + expect(cause).toBeInstanceOf(Error); + expect(name).toBe("RuntimeError"); + expect(message).toBeString(); + expect(message).toEqual(eMsg); + expect(stack).toContain(eMsg); + expect(cause?.constructor.name).toEqual("ErrorFromUnknownThrowable"); + + const rexAsJson = stringify(rex); + expect(rexAsJson).toContain(eMsg); + } + }); + + it("avoids losing information of inner exception: 0", () => { + const eMsg = uuidV4(); + + try { + throw 0; + } catch (ex: unknown) { + const rex = createRuntimeErrorWithCause(eMsg, ex); + const { cause, message, name, stack } = rex; + expect(cause).toBeInstanceOf(Error); + expect(name).toBe("RuntimeError"); + expect(message).toBeString(); + expect(message).toEqual(eMsg); + expect(stack).toContain(eMsg); + expect(cause?.constructor.name).toEqual("ErrorFromUnknownThrowable"); + + const rexAsJson = stringify(rex); + expect(rexAsJson).toContain(eMsg); + } + }); + + it("avoids losing information of inner exception: empty POJO", () => { + const eMsg = uuidV4(); + + try { + throw {}; + } catch (ex: unknown) { + const rex = createRuntimeErrorWithCause(eMsg, ex); + const { cause, message, name, stack } = rex; + expect(cause).toBeInstanceOf(Error); + expect(name).toBe("RuntimeError"); + expect(message).toBeString(); + expect(message).toEqual(eMsg); + expect(stack).toContain(eMsg); + expect(cause?.constructor.name).toEqual("ErrorFromUnknownThrowable"); + + const rexAsJson = stringify(rex); + expect(rexAsJson).toContain(eMsg); + } + }); + + it("avoids losing information of inner exception: empty array", () => { + const eMsg = uuidV4(); + + try { + throw []; + } catch (ex: unknown) { + const rex = createRuntimeErrorWithCause(eMsg, ex); + const { cause, message, name, stack } = rex; + expect(cause).toBeInstanceOf(Error); + expect(name).toBe("RuntimeError"); + expect(message).toBeString(); + expect(message).toEqual(eMsg); + expect(stack).toContain(eMsg); + expect(cause?.constructor.name).toEqual("ErrorFromUnknownThrowable"); + + const rexAsJson = stringify(rex); + expect(rexAsJson).toContain(eMsg); + } + }); + + it("avoids losing information of inner exception: filled array", () => { + const eMsg = uuidV4(); + const id1 = uuidV4(); + const id2 = uuidV4(); + const id3 = uuidV4(); + + try { + throw [id1, id2, id3]; + } catch (ex: unknown) { + const rex = createRuntimeErrorWithCause(eMsg, ex); + const { cause, message, name, stack } = rex; + expect(cause).toBeInstanceOf(Error); + expect(name).toBe("RuntimeError"); + expect(message).toBeString(); + expect(message).toEqual(eMsg); + expect(stack).toContain(eMsg); + expect(cause?.constructor.name).toEqual("ErrorFromUnknownThrowable"); + + const rexAsJson = stringify(rex); + expect(rexAsJson).toContain(eMsg); + expect(rexAsJson).toContain(id1); + expect(rexAsJson).toContain(id2); + expect(rexAsJson).toContain(id3); + } + }); + + it("avoids losing information of inner exception: Symbol", () => { + const eMsg = uuidV4(); + const id1 = uuidV4(); + + try { + const symbolToThrow = Symbol(id1); + throw symbolToThrow; + } catch (ex: unknown) { + const rex = createRuntimeErrorWithCause(eMsg, ex); + const { cause, message, name, stack } = rex; + expect(cause).toBeInstanceOf(Error); + expect(name).toBe("RuntimeError"); + expect(message).toBeString(); + expect(message).toEqual(eMsg); + expect(stack).toContain(eMsg); + expect(cause?.constructor.name).toEqual("ErrorFromSymbol"); + + const rexAsJson = stringify(rex); + expect(rexAsJson).toContain(eMsg); + expect(rexAsJson).toContain(id1); + } + }); + + it("avoids losing information of inner exception: BigInt", () => { + const eMsg = uuidV4(); + // BigInt(Number.MAX_SAFE_INTEGER) * BigInt(Number.MAX_SAFE_INTEGER); + // => + // 81129638414606663681390495662081n + const maxSafeIntSquaredAsStr = "81129638414606663681390495662081"; + const maxSafeIntSquared = BigInt(maxSafeIntSquaredAsStr); + + try { + throw maxSafeIntSquared; + } catch (ex: unknown) { + const rex = createRuntimeErrorWithCause(eMsg, ex); + const { cause, message, name, stack } = rex; + expect(cause).toBeInstanceOf(Error); + expect(name).toBe("RuntimeError"); + expect(message).toBeString(); + expect(message).toEqual(eMsg); + expect(stack).toContain(eMsg); + expect(cause?.constructor.name).toEqual("ErrorFromUnknownThrowable"); + + const rexAsJson = stringify(rex); + expect(rexAsJson).toContain(eMsg); + expect(rexAsJson).toContain(maxSafeIntSquaredAsStr); + } + }); + + it("avoids losing information of inner exception: Int32", () => { + const throwable: number = Math.random() * 10e7; + const eMsg = uuidV4(); + try { + throw throwable; + } catch (ex: unknown) { + const rex = createRuntimeErrorWithCause(eMsg, ex); + const { cause, message, name, stack } = rex; + expect(cause).toBeInstanceOf(Error); + expect(name).toBe("RuntimeError"); + expect(message).toBeString(); + expect(message).toEqual(eMsg); + expect(stack).toContain(eMsg); + expect(cause?.constructor.name).toEqual("ErrorFromUnknownThrowable"); + + const rexAsJson = stringify(rex); + expect(rexAsJson).toContain(eMsg); + } + }); + + it("avoids losing information of inner exception: String", () => { + const eMsg = uuidV4(); + try { + throw eMsg; + } catch (ex: unknown) { + const rex = createRuntimeErrorWithCause(eMsg, ex); + const { cause, message, name, stack } = rex; + expect(cause).toBeInstanceOf(Error); + expect(name).toBe("RuntimeError"); + expect(message).toBeString(); + expect(message).toEqual(eMsg); + expect(stack).toContain(eMsg); + expect(cause?.constructor.name).toEqual("ErrorFromUnknownThrowable"); + + const rexAsJson = stringify(rex); + expect(rexAsJson).toContain(eMsg); + } + }); +});