-
Notifications
You must be signed in to change notification settings - Fork 286
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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]>
- Loading branch information
Showing
9 changed files
with
600 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,6 +33,11 @@ | |
"name": "Peter Somogyvari", | ||
"email": "[email protected]", | ||
"url": "https://accenture.com" | ||
}, | ||
{ | ||
"name": "Michael Courtin", | ||
"email": "[email protected]", | ||
"url": "https://accenture.com" | ||
} | ||
], | ||
"main": "dist/lib/main/typescript/index.js", | ||
|
46 changes: 46 additions & 0 deletions
46
packages/cactus-common/src/main/typescript/exception/coerce-unknown-to-error.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} |
104 changes: 104 additions & 0 deletions
104
packages/cactus-common/src/main/typescript/exception/create-runtime-error-with-cause.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<number> { | ||
* 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); | ||
} |
1 change: 1 addition & 0 deletions
1
packages/cactus-common/src/main/typescript/exception/error-from-symbol.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export class ErrorFromSymbol extends Error {} |
18 changes: 18 additions & 0 deletions
18
packages/cactus-common/src/main/typescript/exception/error-from-unknown-throwable.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export function hasKey<T extends string>( | ||
x: unknown, | ||
key: T, | ||
): x is { [key in T]: unknown } { | ||
return Boolean(typeof x === "object" && x && key in x); | ||
} |
Oops, something went wrong.