diff --git a/.changeset/fix-formatjson-error-message.md b/.changeset/fix-formatjson-error-message.md new file mode 100644 index 00000000000..efb9ee61976 --- /dev/null +++ b/.changeset/fix-formatjson-error-message.md @@ -0,0 +1,11 @@ +--- +"effect": patch +--- + +Fix `Formatter.formatJson` to include the message when stringifying plain `Error` values, matching the output of `Logger.formatStructured`. + +```ts +import { Formatter } from "effect" + +Formatter.formatJson(new Error("boom")) // now `"Error: boom"`, previously `{}` +``` diff --git a/packages/effect/src/Formatter.ts b/packages/effect/src/Formatter.ts index 9121c252de6..92c09279838 100644 --- a/packages/effect/src/Formatter.ts +++ b/packages/effect/src/Formatter.ts @@ -316,6 +316,9 @@ export function formatJson(input: unknown, options?: { if (typeof redacted !== "object" || redacted === null) { return redacted } + if (redacted instanceof Error && !Predicate.hasProperty(redacted, "toJSON")) { + return safeToString(redacted) + } while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) { ancestors.pop() } diff --git a/packages/effect/test/Formatter.test.ts b/packages/effect/test/Formatter.test.ts index acbfcbabdf7..db821a547bf 100644 --- a/packages/effect/test/Formatter.test.ts +++ b/packages/effect/test/Formatter.test.ts @@ -344,6 +344,20 @@ describe("Formatter", () => { strictEqual(formatJson([1n, 2n]), `["1n","2n"]`) }) + it("should stringify Error messages", () => { + strictEqual(formatJson(new Error("boom")), `"Error: boom"`) + strictEqual(formatJson({ error: new Error("boom") }), `{"error":"Error: boom"}`) + }) + + it("should keep structured serialization for Errors that define toJSON", () => { + class Tagged extends Error { + toJSON() { + return { _tag: "Tagged", message: "boom" } + } + } + strictEqual(formatJson(new Tagged("boom")), `{"_tag":"Tagged","message":"boom"}`) + }) + it("should redact sensitive data", () => { const date = Object.assign(new Date(0), { [Redactable.symbolRedactable]: () => "[REDACTED]" diff --git a/packages/effect/test/Logger.test.ts b/packages/effect/test/Logger.test.ts index e90a0e595eb..c227dee636a 100644 --- a/packages/effect/test/Logger.test.ts +++ b/packages/effect/test/Logger.test.ts @@ -115,6 +115,24 @@ describe("Logger", () => { assert.strictEqual(json[0].level, "INFO") })) + it.effect("formatJson includes the message of plain Errors", () => + Effect.gen(function*() { + const json: Array<{ readonly message: unknown; readonly level: string }> = [] + const logger = Logger.formatJson.pipe(Logger.map((output) => void json.push(JSON.parse(output)))) + + yield* Effect.gen(function*() { + yield* Effect.fail(new Error("boom")) + }).pipe( + Effect.tapError(Effect.logError), + Effect.ignore, + Effect.provide(Logger.layer([logger])) + ) + + assert.strictEqual(json.length, 1) + assert.strictEqual(json[0].message, "Error: boom") + assert.strictEqual(json[0].level, "ERROR") + })) + it.effect("annotateLogsScoped applies annotations only while scoped", () => Effect.gen(function*() { const annotations: Array> = []