Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/fix-formatjson-error-message.md
Original file line number Diff line number Diff line change
@@ -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 `{}`
```
3 changes: 3 additions & 0 deletions packages/effect/src/Formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
14 changes: 14 additions & 0 deletions packages/effect/test/Formatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]"
Expand Down
18 changes: 18 additions & 0 deletions packages/effect/test/Logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,24 @@
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"))

Check failure on line 124 in packages/effect/test/Logger.test.ts

View workflow job for this annotation

GitHub Actions / Types

This Effect never succeeds; using `return yield*` preserves a definitive generator exit point for type narrowing and tooling support. effect(missingReturnYieldStar)
}).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<Record<string, unknown>> = []
Expand Down