This commit addresses the issue of Chromium v126 and later not displaying error messages correctly when the error object's `message` property uses a getter. It refactors the code to utilize an immutable Error object with recursive context, improves error message formatting and leverages the `cause` property. Changes: - Refactor error wrapping internals to use an immutable error object, eliminating `message` getters. - Utilize the `cause` property in contextual errors for enhanced error display in the console. - Enhance message formatting with better indentation and listing. - Improve clarity by renaming values thrown during validations.
117 lines
3.3 KiB
TypeScript
117 lines
3.3 KiB
TypeScript
import { CustomError } from '@/application/Common/CustomError';
|
|
import { indentText } from '@/application/Common/Text/IndentText';
|
|
|
|
export interface ErrorWithContextWrapper {
|
|
(
|
|
innerError: Error,
|
|
additionalContext: string,
|
|
): Error;
|
|
}
|
|
|
|
export const wrapErrorWithAdditionalContext: ErrorWithContextWrapper = (
|
|
innerError,
|
|
additionalContext,
|
|
) => {
|
|
if (!additionalContext) {
|
|
throw new Error('Missing additional context');
|
|
}
|
|
return new ContextualError({
|
|
innerError,
|
|
additionalContext,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Class for building a detailed error trace.
|
|
*
|
|
* Alternatives considered:
|
|
* - `AggregateError`:
|
|
* Similar but not well-serialized or displayed by browsers such as Chromium (last tested v126).
|
|
* - `cause` property:
|
|
* Not displayed by all browsers (last tested v126).
|
|
* Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
|
|
*
|
|
* This is immutable where the constructor sets the values because using getter functions such as
|
|
* `get cause()`, `get message()` does not work on Chromium (last tested v126), but works fine on
|
|
* Firefox (last tested v127).
|
|
*/
|
|
class ContextualError extends CustomError {
|
|
constructor(public readonly context: ErrorContext) {
|
|
super(
|
|
generateDetailedErrorMessageWithContext(context),
|
|
{
|
|
cause: context.innerError,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
interface ErrorContext {
|
|
readonly innerError: Error;
|
|
readonly additionalContext: string;
|
|
}
|
|
|
|
function generateDetailedErrorMessageWithContext(
|
|
context: ErrorContext,
|
|
): string {
|
|
return [
|
|
'\n',
|
|
// Display the current error message first, then the root cause.
|
|
// This prevents repetitive main messages for errors with a `cause:` chain,
|
|
// aligning with browser error display conventions.
|
|
context.additionalContext,
|
|
'\n',
|
|
'Error Trace (starting from root cause):',
|
|
indentText(
|
|
formatErrorTrace(
|
|
// Displaying contexts from the top frame (deepest, most recent) aligns with
|
|
// common debugger/compiler standard.
|
|
extractErrorTraceAscendingFromDeepest(context),
|
|
),
|
|
),
|
|
'\n',
|
|
].join('\n');
|
|
}
|
|
|
|
function extractErrorTraceAscendingFromDeepest(
|
|
context: ErrorContext,
|
|
): string[] {
|
|
const originalError = findRootError(context.innerError);
|
|
const contextsDescendingFromMostRecent: string[] = [
|
|
context.additionalContext,
|
|
...gatherContextsFromErrorChain(context.innerError),
|
|
originalError.toString(),
|
|
];
|
|
const contextsAscendingFromDeepest = contextsDescendingFromMostRecent.reverse();
|
|
return contextsAscendingFromDeepest;
|
|
}
|
|
|
|
function findRootError(error: Error): Error {
|
|
if (error instanceof ContextualError) {
|
|
return findRootError(error.context.innerError);
|
|
}
|
|
return error;
|
|
}
|
|
|
|
function gatherContextsFromErrorChain(
|
|
error: Error,
|
|
accumulatedContexts: string[] = [],
|
|
): string[] {
|
|
if (error instanceof ContextualError) {
|
|
accumulatedContexts.push(error.context.additionalContext);
|
|
return gatherContextsFromErrorChain(error.context.innerError, accumulatedContexts);
|
|
}
|
|
return accumulatedContexts;
|
|
}
|
|
|
|
function formatErrorTrace(
|
|
errorMessages: readonly string[],
|
|
): string {
|
|
if (errorMessages.length === 1) {
|
|
return errorMessages[0];
|
|
}
|
|
return errorMessages
|
|
.map((context, index) => `${index + 1}.${indentText(context)}`)
|
|
.join('\n');
|
|
}
|