Improve compiler error display for latest Chromium
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.
This commit is contained in:
@@ -31,7 +31,7 @@ function validateCollectionsData(
|
||||
) {
|
||||
validator.assertNonEmptyCollection({
|
||||
value: collections,
|
||||
valueName: 'collections',
|
||||
valueName: 'Collections',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -45,14 +45,14 @@ function validateCollection(
|
||||
): void {
|
||||
validator.assertObject({
|
||||
value: content,
|
||||
valueName: 'collection',
|
||||
valueName: 'Collection',
|
||||
allowedProperties: [
|
||||
'os', 'scripting', 'actions', 'functions',
|
||||
],
|
||||
});
|
||||
validator.assertNonEmptyCollection({
|
||||
value: content.actions,
|
||||
valueName: '"actions" in collection',
|
||||
valueName: '\'actions\' in collection',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +1,116 @@
|
||||
import { CustomError } from '@/application/Common/CustomError';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
|
||||
export interface ErrorWithContextWrapper {
|
||||
(
|
||||
error: Error,
|
||||
innerError: Error,
|
||||
additionalContext: string,
|
||||
): Error;
|
||||
}
|
||||
|
||||
export const wrapErrorWithAdditionalContext: ErrorWithContextWrapper = (
|
||||
error: Error,
|
||||
additionalContext: string,
|
||||
innerError,
|
||||
additionalContext,
|
||||
) => {
|
||||
return (error instanceof ContextualError ? error : new ContextualError(error))
|
||||
.withAdditionalContext(additionalContext);
|
||||
if (!additionalContext) {
|
||||
throw new Error('Missing additional context');
|
||||
}
|
||||
return new ContextualError({
|
||||
innerError,
|
||||
additionalContext,
|
||||
});
|
||||
};
|
||||
|
||||
/* AggregateError is similar but isn't well-serialized or displayed by browsers */
|
||||
/**
|
||||
* 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 {
|
||||
private readonly additionalContext = new Array<string>();
|
||||
|
||||
constructor(
|
||||
public readonly innerError: Error,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public withAdditionalContext(additionalContext: string): this {
|
||||
this.additionalContext.push(additionalContext);
|
||||
return this;
|
||||
}
|
||||
|
||||
public get message(): string { // toString() is not used when Chromium logs it on console
|
||||
return [
|
||||
'\n',
|
||||
this.innerError.message,
|
||||
'\n',
|
||||
'Additional context:',
|
||||
...this.additionalContext.map((context, index) => `${index + 1}: ${context}`),
|
||||
].join('\n');
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ function assertArray(
|
||||
valueName: string,
|
||||
): asserts value is Array<unknown> {
|
||||
if (!isArray(value)) {
|
||||
throw new Error(`'${valueName}' should be of type 'array', but is of type '${typeof value}'.`);
|
||||
throw new Error(`${valueName} should be of type 'array', but is of type '${typeof value}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ function assertString(
|
||||
valueName: string,
|
||||
): asserts value is string {
|
||||
if (!isString(value)) {
|
||||
throw new Error(`'${valueName}' should be of type 'string', but is of type '${typeof value}'.`);
|
||||
throw new Error(`${valueName} should be of type 'string', but is of type '${typeof value}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ function ensureValidCategory(
|
||||
});
|
||||
validator.assertType((v) => v.assertObject({
|
||||
value: category,
|
||||
valueName: category.category ?? 'category',
|
||||
valueName: `Category '${category.category}'` ?? 'Category',
|
||||
allowedProperties: [
|
||||
'docs', 'children', 'category',
|
||||
],
|
||||
|
||||
@@ -22,7 +22,7 @@ export const createFunctionCallArgument: FunctionCallArgumentFactory = (
|
||||
utilities.validateParameterName(parameterName);
|
||||
utilities.typeValidator.assertNonEmptyString({
|
||||
value: argumentValue,
|
||||
valueName: `Missing argument value for the parameter "${parameterName}".`,
|
||||
valueName: `Function parameter '${parameterName}'`,
|
||||
});
|
||||
return {
|
||||
parameterName,
|
||||
|
||||
@@ -42,7 +42,7 @@ function getCallSequence(calls: FunctionCallsData, validator: TypeValidator): Fu
|
||||
if (isArray(calls)) {
|
||||
validator.assertNonEmptyCollection({
|
||||
value: calls,
|
||||
valueName: 'function call sequence',
|
||||
valueName: 'Function call sequence',
|
||||
});
|
||||
return calls as FunctionCallData[];
|
||||
}
|
||||
@@ -56,7 +56,7 @@ function parseFunctionCall(
|
||||
): FunctionCall {
|
||||
utilities.typeValidator.assertObject({
|
||||
value: call,
|
||||
valueName: 'function call',
|
||||
valueName: 'Function call',
|
||||
allowedProperties: ['function', 'parameters'],
|
||||
});
|
||||
const callArgs = parseArgs(call.parameters, utilities.createCallArgument);
|
||||
|
||||
@@ -13,7 +13,7 @@ export const validateParameterName = (
|
||||
) => {
|
||||
typeValidator.assertNonEmptyString({
|
||||
value: parameterName,
|
||||
valueName: 'parameter name',
|
||||
valueName: 'Parameter name',
|
||||
rule: {
|
||||
expectedMatch: /^[0-9a-zA-Z]+$/,
|
||||
errorMessage: `parameter name must be alphanumeric but it was "${parameterName}".`,
|
||||
|
||||
@@ -102,7 +102,7 @@ function validateScript(
|
||||
): asserts script is NonNullable<ScriptData> {
|
||||
validator.assertType((v) => v.assertObject<CallScriptData & CodeScriptData>({
|
||||
value: script,
|
||||
valueName: script.name ?? 'script',
|
||||
valueName: `Script '${script.name}'` ?? 'Script',
|
||||
allowedProperties: [
|
||||
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
|
||||
],
|
||||
|
||||
@@ -37,7 +37,7 @@ function validateData(
|
||||
): void {
|
||||
validator.assertObject({
|
||||
value: data,
|
||||
valueName: 'scripting definition',
|
||||
valueName: 'Scripting definition',
|
||||
allowedProperties: ['language', 'fileExtension', 'startCode', 'endCode'],
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user