diff --git a/.changeset/petite-doodles-help.md b/.changeset/petite-doodles-help.md new file mode 100644 index 00000000000..5bb8bdfbe31 --- /dev/null +++ b/.changeset/petite-doodles-help.md @@ -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/@vitest+runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js:155:11 + at file:///pj/effect/effect/node_modules/.pnpm/@vitest+runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js:752:26 + at file:///pj/effect/effect/node_modules/.pnpm/@vitest+runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js:1897:20 + at new Promise () + at runWithTimeout (file:///pj/effect/effect/node_modules/.pnpm/@vitest+runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js:1863:10) + at runTest (file:///pj/effect/effect/node_modules/.pnpm/@vitest+runner@3.2.4/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/@vitest+runner@3.2.4/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/@vitest+runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js:155:11 + at file:///pj/effect/effect/node_modules/.pnpm/@vitest+runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js:752:26 + at file:///pj/effect/effect/node_modules/.pnpm/@vitest+runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js:1897:20 + at new Promise () + at runWithTimeout (file:///pj/effect/effect/node_modules/.pnpm/@vitest+runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js:1863:10) + at runTest (file:///pj/effect/effect/node_modules/.pnpm/@vitest+runner@3.2.4/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/@vitest+runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js:1718:7) { + testValue: "1" + } +``` diff --git a/packages/effect/src/Inspectable.ts b/packages/effect/src/Inspectable.ts index 5f397c66357..fec95a4ec67 100644 --- a/packages/effect/src/Inspectable.ts +++ b/packages/effect/src/Inspectable.ts @@ -102,13 +102,58 @@ 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 = [] - const retVal = JSON.stringify( + const retVal = stringifyWithDepth( obj, + depth, (_key, value) => typeof value === "object" && value !== null ? cache.includes(value) @@ -116,6 +161,8 @@ export const stringifyCircular = (obj: unknown, whitespace?: number | string | u : cache.push(value) && (redactableState.fiberRefs !== undefined && isRedactable(value) ? value[symbolRedactable](redactableState.fiberRefs) : value) + : typeof value === "bigint" + ? value.toString() : value, whitespace ) diff --git a/packages/effect/src/internal/cause.ts b/packages/effect/src/internal/cause.ts index 881ef5b6d73..1576db030b1 100644 --- a/packages/effect/src/internal/cause.ts +++ b/packages/effect/src/internal/cause.ts @@ -876,22 +876,43 @@ export const pretty = (cause: Cause.Cause, options?: { return "All fibers interrupted without errors." } return prettyErrors(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]+(? { 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 } }) }) @@ -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" ]) })