Skip to content

Commit b3a508c

Browse files
m-courtinpetermetz
authored andcommitted
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 <[email protected]> Closes: #1702 [skip ci] Signed-off-by: Michael Courtin <[email protected]> Signed-off-by: Peter Somogyvari <[email protected]>
1 parent 8255134 commit b3a508c

File tree

9 files changed

+600
-38
lines changed

9 files changed

+600
-38
lines changed

packages/cactus-cmd-api-server/src/main/typescript/api-server.ts

+31-38
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
Bools,
4949
Logger,
5050
LoggerProvider,
51+
newRex,
5152
Servers,
5253
} from "@hyperledger/cactus-common";
5354

@@ -248,17 +249,17 @@ export class ApiServer {
248249
}
249250

250251
return { addressInfoCockpit, addressInfoApi, addressInfoGrpc };
251-
} catch (ex) {
252-
const errorMessage = `Failed to start ApiServer: ${ex.stack}`;
253-
this.log.error(errorMessage);
252+
} catch (ex1: unknown) {
253+
const context = "Failed to start ApiServer";
254+
this.log.error(context, ex1);
254255
this.log.error(`Attempting shutdown...`);
255256
try {
256257
await this.shutdown();
257258
this.log.info(`Server shut down after crash OK`);
258-
} catch (ex) {
259-
this.log.error(ApiServer.E_POST_CRASH_SHUTDOWN, ex);
259+
} catch (ex2: unknown) {
260+
this.log.error(ApiServer.E_POST_CRASH_SHUTDOWN, ex2);
260261
}
261-
throw new Error(errorMessage);
262+
throw newRex(context, ex1);
262263
}
263264
}
264265

@@ -304,11 +305,11 @@ export class ApiServer {
304305
await this.getPluginImportsCount(),
305306
);
306307
return this.pluginRegistry;
307-
} catch (e) {
308+
} catch (ex: unknown) {
308309
this.pluginRegistry = new PluginRegistry({ plugins: [] });
309-
const errorMessage = `Failed init PluginRegistry: ${e.stack}`;
310-
this.log.error(errorMessage);
311-
throw new Error(errorMessage);
310+
const context = "Failed to init PluginRegistry";
311+
this.log.debug(context, ex);
312+
throw newRex(context, ex);
312313
}
313314
}
314315

@@ -368,15 +369,10 @@ export class ApiServer {
368369
await plugin.onPluginInit();
369370

370371
return plugin;
371-
} catch (error) {
372-
const errorMessage = `${fnTag} failed instantiating plugin '${packageName}' with the instanceId '${options.instanceId}'`;
373-
this.log.error(errorMessage, error);
374-
375-
if (error instanceof Error) {
376-
throw new RuntimeError(errorMessage, error);
377-
} else {
378-
throw new RuntimeError(errorMessage, JSON.stringify(error));
379-
}
372+
} catch (ex: unknown) {
373+
const context = `${fnTag} failed instantiating plugin '${packageName}' with the instanceId '${options.instanceId}'`;
374+
this.log.debug(context, ex);
375+
throw newRex(context, ex);
380376
}
381377
}
382378

@@ -397,10 +393,10 @@ export class ApiServer {
397393
try {
398394
await fs.mkdirp(pluginPackageDir);
399395
this.log.debug(`${pkgName} plugin package dir: %o`, pluginPackageDir);
400-
} catch (ex) {
401-
const errorMessage =
396+
} catch (ex: unknown) {
397+
const context =
402398
"Could not create plugin installation directory, check the file-system permissions.";
403-
throw new RuntimeError(errorMessage, ex);
399+
throw newRex(context, ex);
404400
}
405401
try {
406402
lmify.setPackageManager("npm");
@@ -418,19 +414,15 @@ export class ApiServer {
418414
// "--ignore-workspace-root-check",
419415
]);
420416
this.log.debug("%o install result: %o", pkgName, out);
421-
if (out.exitCode !== 0) {
422-
throw new RuntimeError("Non-zero exit code: ", JSON.stringify(out));
417+
if (out?.exitCode && out.exitCode !== 0) {
418+
const eMsg = "Non-zero exit code returned by lmify.install() indicating that the underlying npm install OS process had encountered a problem:";
419+
throw newRex(eMsg, out);
423420
}
424421
this.log.info(`Installed ${pkgName} OK`);
425-
} catch (ex) {
426-
const errorMessage = `${fnTag} failed installing plugin '${pkgName}`;
427-
this.log.error(errorMessage, ex);
428-
429-
if (ex instanceof Error) {
430-
throw new RuntimeError(errorMessage, ex);
431-
} else {
432-
throw new RuntimeError(errorMessage, JSON.stringify(ex));
433-
}
422+
} catch (ex: unknown) {
423+
const context = `${fnTag} failed installing plugin '${pkgName}`;
424+
this.log.debug(ex, context);
425+
throw newRex(context, ex);
434426
}
435427
}
436428

@@ -451,24 +443,25 @@ export class ApiServer {
451443
this.log.info(`Stopped ${webServicesShutdown.length} WS plugin(s) OK`);
452444

453445
if (this.httpServerApi?.listening) {
454-
this.log.info(`Closing HTTP server of the API...`);
446+
this.log.info(`Closing Cacti HTTP server of the API...`);
455447
await Servers.shutdown(this.httpServerApi);
456448
this.log.info(`Close HTTP server of the API OK`);
457449
}
458450

459451
if (this.httpServerCockpit?.listening) {
460-
this.log.info(`Closing HTTP server of the cockpit ...`);
452+
this.log.info(`Closing Cacti HTTP server of the cockpit ...`);
461453
await Servers.shutdown(this.httpServerCockpit);
462454
this.log.info(`Close HTTP server of the cockpit OK`);
463455
}
464456

465457
if (this.grpcServer) {
466-
this.log.info(`Closing gRPC server ...`);
458+
this.log.info(`Closing Cacti gRPC server ...`);
467459
await new Promise<void>((resolve, reject) => {
468460
this.grpcServer.tryShutdown((ex?: Error) => {
469461
if (ex) {
470-
this.log.error("Failed to shut down gRPC server: ", ex);
471-
reject(ex);
462+
const eMsg = "Failed to shut down gRPC server of the Cacti API server.";
463+
this.log.debug(eMsg, ex);
464+
reject(newRex(eMsg, ex));
472465
} else {
473466
resolve();
474467
}

packages/cactus-common/package.json

+5
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@
3333
"name": "Peter Somogyvari",
3434
"email": "[email protected]",
3535
"url": "https://accenture.com"
36+
},
37+
{
38+
"name": "Michael Courtin",
39+
"email": "[email protected]",
40+
"url": "https://accenture.com"
3641
}
3742
],
3843
"main": "dist/lib/main/typescript/index.js",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import stringify from "fast-safe-stringify";
2+
import { ErrorFromUnknownThrowable } from "./error-from-unknown-throwable";
3+
import { ErrorFromSymbol } from "./error-from-symbol";
4+
5+
/**
6+
* Safely converts `unknown` to an `Error` with doing a best effort to ensure
7+
* that root cause analysis information is not lost. The idea here is to help
8+
* people who are reading logs of errors while trying to figure out what went
9+
* wrong after a crash.
10+
*
11+
* Often in Javascript this is much harder than it could be due to lack of
12+
* runtime checks by the JSVM (Javascript Virtual Machine) on the values/objects
13+
* that are being thrown.
14+
*
15+
* @param x The value/object whose type information is completely unknown at
16+
* compile time, such as the input parameter of a catch block (which could
17+
* be anything because the JS runtime has no enforcement on it at all, e.g.
18+
* you can throw null, undefined, empty strings of whatever else you'd like.)
19+
* @returns An `Error` object that is the original `x` if it was an `Error`
20+
* instance to begin with or a stringified JSON representation of `x` otherwise.
21+
*/
22+
export function coerceUnknownToError(x: unknown): Error {
23+
if (typeof x === "symbol") {
24+
const symbolAsStr = x.toString();
25+
return new ErrorFromSymbol(symbolAsStr);
26+
} else if (x instanceof Error) {
27+
return x;
28+
} else {
29+
const xAsJson = stringify(x, (_, value) =>
30+
typeof value === "bigint" ? value.toString() + "n" : value,
31+
);
32+
return new ErrorFromUnknownThrowable(xAsJson);
33+
}
34+
}
35+
36+
/**
37+
* This is an alias to `coerceUnknownToError(x: unknown)`.
38+
*
39+
* The shorter name allows for different style choices to be made by the person
40+
* writing the error handling code.
41+
*
42+
* @see #coerceUnknownToError
43+
*/
44+
export function asError(x: unknown): Error {
45+
return coerceUnknownToError(x);
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { RuntimeError } from "run-time-error";
2+
import { coerceUnknownToError } from "./coerce-unknown-to-error";
3+
4+
/**
5+
* ### STANDARD EXCEPTION HANDLING - EXAMPLE WITH RE-THROW:
6+
*
7+
* Use the this utility function and pass in any throwable of whatever type and format
8+
* The underlying implementation will take care of determining if it's a valid
9+
* `Error` instance or not and act accordingly with avoding information loss
10+
* being the number one priority.
11+
*
12+
* You can perform a fast-fail re-throw with additional context like the snippet
13+
* below.
14+
* Notice that we log on the debug level inside the catch block to make sure that
15+
* if somebody higher up in the callstack ends up handling this exception then
16+
* it will never get logged on the error level which is good because if it did
17+
* that would be a false-positive, annoying system administrators who have to
18+
* figure out which errors in their production logs need to be ignored and which
19+
* ones are legitimate.
20+
* The trade-off with the above is trust: Specifically, we are trusting the
21+
* person above us in the callstack to either correctly handle the exception
22+
* or make sure that it does get logged on the error level. If they fail to do
23+
* either one of those, then we'll have silent failures on our hand that will
24+
* be hard to debug.
25+
* Lack of the above kind of trust is usually what pushes people to just go for
26+
* it and log their caught exceptions on the error level but this most likely
27+
* a mistake in library code where there just isn't enough context to know if
28+
* an error is legitimate or not most of the time. If you are writing application
29+
* logic then it's usually a simpler decision with more information at your
30+
* disposal.
31+
*
32+
* The underlying concept is that if you log something on an error level, you
33+
* indicate that another human should fix a bug that is in the code. E.g.,
34+
* when they see the error logs, they should go and fix something.
35+
*
36+
* ```typescript
37+
* public doSomething(): void {
38+
* try {
39+
* someSubTaskToExecute();
40+
* } catch (ex) {
41+
* const eMsg = "Failed to run **someSubTask** while doing **something**:"
42+
* this.log.debug(eMsg, ex);
43+
* throw createRuntimeErrorWithCause(eMsg, ex);
44+
* }
45+
* ```
46+
*
47+
* ### EXCEPTION HANDLING WITH CONDITIONAL HANDLING AND RE-THROW - EXAMPLE:
48+
*
49+
* In case you need to do a conditional exception-handling:
50+
* - Use the RuntimeError to re-throw and
51+
* provide the previous exception as cause in the new RuntimeError to retain
52+
* the information and distinguish between an exception you can handle and
53+
* recover from and one you can't
54+
*
55+
* ```typescript
56+
* public async doSomething(): Promise<number> {
57+
* try {
58+
* await doSubTaskThatsAPartOfDoingSomething();
59+
* } catch (ex) {
60+
* if (ex instanceof MyErrorThatICanHandleAndRecoverFrom) {
61+
* // An exception with a fixable scenario we can recover from thru an additional handling
62+
* // do something here to handle and fix the issue
63+
* // where "fixing" means that the we end up recovering
64+
* // OK instead of having to crash. Recovery means that
65+
* // we are confident that the second sub-task is safe to proceed with
66+
* // despite of the error that was caught here
67+
* this.log.debug("We've got an failure in 'doSubTaskThatsAPartOfDoingSomething()' but we could fix it and recover to continue".);
68+
* } else {
69+
* // An "unexpected exception" where we want to fail immediately
70+
* // to avoid follow-up problems
71+
* const context = "We got an severe failure in 'doSubTaskThatsAPartOfDoingSomething()' and need to stop directly here to avoid follow-up problems";
72+
* this.log.erorr(context, ex);
73+
* throw newRex(context, ex);
74+
* }
75+
* }
76+
* const result = await doSecondAndFinalSubTask();
77+
* return result; // 42
78+
* }
79+
* ```
80+
*
81+
* @param message The contextual information that will be passed into the
82+
* constructor of the returned {@link RuntimeError} instance.
83+
* @param cause The caught throwable which we do not know the exact type of but
84+
* need to make sure that whatever information is in t here is not lost.
85+
* @returns The instance that has the combined information of the input parameters.
86+
*/
87+
export function createRuntimeErrorWithCause(
88+
message: string,
89+
cause: unknown,
90+
): RuntimeError {
91+
const innerEx = coerceUnknownToError(cause);
92+
return new RuntimeError(message, innerEx);
93+
}
94+
95+
/**
96+
* An alias to the `createRuntimeErrorWithCause` function for those prefering
97+
* a shorter utility for their personal style.
98+
*
99+
* @see {@link createRuntimeErrorWithCause}
100+
* @returns `RuntimeError`
101+
*/
102+
export function newRex(message: string, cause: unknown): RuntimeError {
103+
return createRuntimeErrorWithCause(message, cause);
104+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export class ErrorFromSymbol extends Error {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* A custom `Error` class designed to encode information about the origin of
3+
* the information contained inside.
4+
*
5+
* Specifically this class is to be used when a catch block has encountered a
6+
* throwable [1] that was not an instance of `Error`.
7+
*
8+
* This should help people understand the contents a little more while searching
9+
* for the root cause of a crash (by letting them know that we had encoutnered
10+
* a non-Error catch block parameter and we wrapped it in this `Error` sub-class
11+
* purposefully to make it easier to deal with it)
12+
*
13+
* [1]: A throwable is a value or object that is possible to be thrown in the
14+
* place of an `Error` object. This - as per the rules of Javascript - can be
15+
* literally anything, NaN, undefined, null, etc.
16+
*/
17+
export class ErrorFromUnknownThrowable extends Error {
18+
}

packages/cactus-common/src/main/typescript/public-api.ts

+6
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,9 @@ export {
2727
} from "./authzn/i-jose-fitting-jwt-params";
2828

2929
export { isRecord } from "./types/is-record";
30+
export { hasKey } from "./types/has-key";
31+
32+
export { asError, coerceUnknownToError } from "./exception/coerce-unknown-to-error";
33+
export { createRuntimeErrorWithCause, newRex } from "./exception/create-runtime-error-with-cause";
34+
export { ErrorFromUnknownThrowable } from "./exception/error-from-unknown-throwable";
35+
export { ErrorFromSymbol } from "./exception/error-from-symbol";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export function hasKey<T extends string>(
2+
x: unknown,
3+
key: T,
4+
): x is { [key in T]: unknown } {
5+
return Boolean(typeof x === "object" && x && key in x);
6+
}

0 commit comments

Comments
 (0)