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.
This commit is contained in:
Peter Steinberger
2026-08-18 00:42:27 -07:00
committed by GitHub
parent 9ad1c0c682
commit 5db8368f35
2 changed files with 33 additions and 1 deletions
@@ -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");
@@ -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: <cause.message>"),
// 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));