From 5db8368f35a56fedfffa6a0ef09f07e599accfd8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 18 Aug 2026 00:42:27 -0700 Subject: [PATCH] fix(errors): drop cause text the message already states (#125687) * fix(errors): drop cause text the message already states Cause-chain dedupe compared whole strings, so a wrapper that embeds its cause verbatim printed it twice, and an errno detail was followed by its own bare code. Skip any cause segment already contained in the accumulated message. * test(backup): stop pinning the duplicated errno suffix Both debug-view assertions required the bare code to follow the errno detail that already names it. Assert the detail itself instead, so they pin the message content rather than the redundancy. * fix(errors): narrow cause dedupe to wrapper-embedded messages Suppressing every contained segment also dropped trailing bare codes, which cron, fs, and backup tests pin deliberately as this formatter's convention. Restrict containment to cause messages so the wrapper duplication is fixed without changing the code suffix, and restore the backup assertions. --- .../src/error-coercion.test.ts | 21 +++++++++++++++++++ .../normalization-core/src/error-coercion.ts | 13 +++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/normalization-core/src/error-coercion.test.ts b/packages/normalization-core/src/error-coercion.test.ts index 64a665171b65..fc543fb6fafe 100644 --- a/packages/normalization-core/src/error-coercion.test.ts +++ b/packages/normalization-core/src/error-coercion.test.ts @@ -26,6 +26,27 @@ describe("formatErrorMessage", () => { ); }); + it("omits cause text the wrapper message already spells out", () => { + // Wrappers that embed the cause verbatim printed the whole sentence twice. + const parseFailure = new SyntaxError("JSON5: invalid character 'j' at 1:7"); + const wrapped = new Error(`Failed to parse --file as JSON5: ${parseFailure.message}`, { + cause: parseFailure, + }); + expect(format(wrapped)).toBe( + "Failed to parse --file as JSON5: JSON5: invalid character 'j' at 1:7", + ); + + // Codes keep their own segment even when the detail already names them. + const errno = Object.assign( + new Error("ENOENT: no such file or directory, open '/tmp/missing.json'"), + { code: "ENOENT" }, + ); + const notFound = new Error("--file not found: /tmp/missing.json.", { cause: errno }); + expect(format(notFound)).toBe( + "--file not found: /tmp/missing.json. | ENOENT: no such file or directory, open '/tmp/missing.json' | ENOENT", + ); + }); + it("formats status/code records and structured non-Error causes", () => { expect(format({ status: 500, code: "EPIPE" })).toBe("status=500 code=EPIPE"); expect(format({ status: 404 })).toBe("status=404 code=unknown"); diff --git a/packages/normalization-core/src/error-coercion.ts b/packages/normalization-core/src/error-coercion.ts index 6deeed1300f2..c96cc060e2b7 100644 --- a/packages/normalization-core/src/error-coercion.ts +++ b/packages/normalization-core/src/error-coercion.ts @@ -87,6 +87,17 @@ export function formatErrorMessage(value: unknown, options: FormatErrorMessageOp formatted += ` | ${message}`; seenMessages.add(message); }; + // Wrappers routinely embed the cause verbatim ("failed to parse X: "), + // which exact-match dedupe misses, so the whole sentence prints twice. Codes stay on + // their own: a trailing bare code is this formatter's convention even when the detail + // already names it. + const appendCauseErrorMessage = (message: string | undefined): void => { + if (message && formatted.includes(message)) { + seenMessages.add(message); + return; + } + appendCauseMessage(message); + }; if (options.includeCode) { const code = readProperty(value, "code"); if (typeof code === "string" || typeof code === "number") { @@ -96,7 +107,7 @@ export function formatErrorMessage(value: unknown, options: FormatErrorMessageOp while (cause && !seen.has(cause)) { seen.add(cause); if (cause instanceof Error) { - appendCauseMessage(cause.message); + appendCauseErrorMessage(cause.message); const code = readProperty(cause, "code"); if (typeof code === "string" || typeof code === "number") { appendCauseMessage(String(code));