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
49 changes: 49 additions & 0 deletions .changeset/petite-doodles-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
"effect": patch
---

improve: Enhance Cause.pretty output with additional error fields, similar to classic throw of Error.
also allows for printing `bigint` values as string.

code:

```
class MyError extends Error {
testValue = BigInt(1)
}
console.log(Cause.pretty(Cause.die(new MyError("my message")), { renderErrorCause: true }))
```

before:

```
Error: my message
at /pj/effect/effect/packages/effect/test/Cause.test.ts:1079:51
at file:///pj/effect/effect/node_modules/.pnpm/@[email protected]/node_modules/@vitest/runner/dist/chunk-hooks.js:155:11
at file:///pj/effect/effect/node_modules/.pnpm/@[email protected]/node_modules/@vitest/runner/dist/chunk-hooks.js:752:26
at file:///pj/effect/effect/node_modules/.pnpm/@[email protected]/node_modules/@vitest/runner/dist/chunk-hooks.js:1897:20
at new Promise (<anonymous>)
at runWithTimeout (file:///pj/effect/effect/node_modules/.pnpm/@[email protected]/node_modules/@vitest/runner/dist/chunk-hooks.js:1863:10)
at runTest (file:///pj/effect/effect/node_modules/.pnpm/@[email protected]/node_modules/@vitest/runner/dist/chunk-hooks.js:1574:12)
at processTicksAndRejections (node:internal/process/task_queues:105:5)
at async Promise.all (index 0)
at runSuite (file:///pj/effect/effect/node_modules/.pnpm/@[email protected]/node_modules/@vitest/runner/dist/chunk-hooks.js:1718:7)
```

after:

```
Error: my message
at /pj/effect/effect/packages/effect/test/Cause.test.ts:1079:51
at file:///pj/effect/effect/node_modules/.pnpm/@[email protected]/node_modules/@vitest/runner/dist/chunk-hooks.js:155:11
at file:///pj/effect/effect/node_modules/.pnpm/@[email protected]/node_modules/@vitest/runner/dist/chunk-hooks.js:752:26
at file:///pj/effect/effect/node_modules/.pnpm/@[email protected]/node_modules/@vitest/runner/dist/chunk-hooks.js:1897:20
at new Promise (<anonymous>)
at runWithTimeout (file:///pj/effect/effect/node_modules/.pnpm/@[email protected]/node_modules/@vitest/runner/dist/chunk-hooks.js:1863:10)
at runTest (file:///pj/effect/effect/node_modules/.pnpm/@[email protected]/node_modules/@vitest/runner/dist/chunk-hooks.js:1574:12)
at processTicksAndRejections (node:internal/process/task_queues:105:5)
at async Promise.all (index 0)
at runSuite (file:///pj/effect/effect/node_modules/.pnpm/@[email protected]/node_modules/@vitest/runner/dist/chunk-hooks.js:1718:7) {
testValue: "1"
}
```
51 changes: 49 additions & 2 deletions packages/effect/src/Inspectable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,20 +102,67 @@ export const toStringUnknown = (u: unknown, whitespace: number | string | undefi
}
}

function stringifyWithDepth(
input: any,
depth?: number,
replacer?: (this: any, key: string, value: any) => any,
whitespace?: string | number
): string {
if (depth === undefined) {
return JSON.stringify(input, replacer, whitespace)
}
if (!input) {
return input
}

const objectsAlreadySerialized = [input],
objDepth = [input]

return JSON.stringify(input, function(key, value) {
if (replacer) {
value = replacer.call(this, key, value)
}
if (key) {
if (typeof value === "object") {
if (objectsAlreadySerialized.indexOf(value) !== -1) {
return undefined
}

objectsAlreadySerialized.push(value)
}

if (objDepth.indexOf(this) === -1) {
objDepth.push(this)
} else {while (objDepth[objDepth.length - 1] !== this) {
objDepth.pop()
}}

if (objDepth.length > depth) {
return undefined
}
}

return value
}, whitespace)
}

/**
* @since 2.0.0
*/
export const stringifyCircular = (obj: unknown, whitespace?: number | string | undefined): string => {
export const stringifyCircular = (obj: unknown, whitespace?: number | string | undefined, depth?: number): string => {
let cache: Array<unknown> = []
const retVal = JSON.stringify(
const retVal = stringifyWithDepth(
obj,
depth,
(_key, value) =>
typeof value === "object" && value !== null
? cache.includes(value)
? undefined // circular reference
: cache.push(value) && (redactableState.fiberRefs !== undefined && isRedactable(value)
? value[symbolRedactable](redactableState.fiberRefs)
: value)
: typeof value === "bigint"
? value.toString()
: value,
whitespace
)
Expand Down
35 changes: 28 additions & 7 deletions packages/effect/src/internal/cause.ts
Original file line number Diff line number Diff line change
Expand Up @@ -876,22 +876,43 @@ export const pretty = <E>(cause: Cause.Cause<E>, options?: {
return "All fibers interrupted without errors."
}
return prettyErrors<E>(cause).map(function(e) {
if (options?.renderErrorCause !== true || e.cause === undefined) {
if (options?.renderErrorCause !== true) {
return e.stack
}
return `${e.stack} {\n${renderErrorCause(e.cause as PrettyError, " ")}\n}`
return renderErrorCause(e, "")
}).join("\n")
}

const renderErrorCause = (cause: PrettyError, prefix: string) => {
const lines = cause.stack!.split("\n")
let stack = `${prefix}[cause]: ${lines[0]}`
const renderErrorCause = (e: Cause.PrettyError, prefix: string) => {
const lines = e.stack!.split("\n")
let stack = `${prefix}${prefix === "" ? "" : "[cause]: "}${lines[0]}`
for (let i = 1, len = lines.length; i < len; i++) {
stack += `\n${prefix}${lines[i]}`
}
if (cause.cause) {
stack += ` {\n${renderErrorCause(cause.cause as PrettyError, `${prefix} `)}\n${prefix}}`
const { cause, message: _, name: __, stack: ___, ...rest } = e
const hasRest = Object.keys(rest).filter((_) => (rest as any)[_] !== undefined).length > 0
if (hasRest || cause) {
stack += ` {`
}
if (hasRest) {
const json = stringifyCircular(toJSON(rest), 2, 2)
const bareJson = json.replace(/^[\t ]*"[^:\n\r]+(?<!\\)":/gm, function(match) {
return match.replace(/"/g, "")
})
for (
const l of bareJson.substring(2, bareJson.length - 1).split("\n").join("\n").trimEnd().split("\n")
) {
stack += `\n${prefix} ${l}`
}
}

if (cause) {
stack += "\n " + renderErrorCause(cause as PrettyError, `${prefix} `)
}
if (hasRest || cause) {
stack += `\n${prefix} }`
}

return stack
}

Expand Down
26 changes: 21 additions & 5 deletions packages/effect/test/Cause.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
Hash,
Inspectable,
Option,
Predicate
Predicate,
Tracer
} from "effect"
import * as internal from "../src/internal/cause.js"
import { causes, equalCauses, errorCauseFunctions, errors } from "./utils/cause.js"
Expand All @@ -43,7 +44,7 @@ describe("Cause", () => {
if (typeof window === "undefined") {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { inspect } = require("node:util")
assertInclude(inspect(ex), "Cause.test.ts:39") // <= reference to the line above
assertInclude(inspect(ex), "Cause.test.ts:40") // <= reference to the line above
}
})
})
Expand Down Expand Up @@ -1064,16 +1065,31 @@ describe("Cause", () => {

describe("Die", () => {
it("with span", () => {
const span = Effect.runSync(Effect.makeSpan("[myspan]"))
const exit: any = Effect.die(new Error("my message", { cause: "my cause" })).pipe(
Effect.withSpan("[myspan]"),
Effect.provideService(Tracer.ParentSpan, span),
Effect.exit,
Effect.runSync
)
const cause = exit.cause
const pretty = Cause.pretty(cause, { renderErrorCause: true })
deepStrictEqual(simplifyStackTrace(pretty), [
const pretty = simplifyStackTrace(Cause.pretty(cause, { renderErrorCause: true }))
deepStrictEqual(pretty, [
`Error: my message`,
"at [myspan]",
"span: {",
"name: \"[myspan]\",",
"parent: {},",
"context: {},",
`startTime: "${(span as any).startTime}",`,
"kind: \"internal\",",
"_tag: \"Span\",",
`spanId: "${span.spanId}",`,
`traceId: "${span.traceId}",`,
"sampled: true,",
"status: {},",
"attributes: {},",
"events: [],",
"links: []",
"[cause]: Error: my cause"
])
})
Expand Down