diff --git a/.changeset/fluffy-bananas-dress.md b/.changeset/fluffy-bananas-dress.md new file mode 100644 index 0000000000..a117201b3a --- /dev/null +++ b/.changeset/fluffy-bananas-dress.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": patch +--- + +preserve defect information in Cause.Die diff --git a/.changeset/real-peaches-brush.md b/.changeset/real-peaches-brush.md new file mode 100644 index 0000000000..068f9e3d4c --- /dev/null +++ b/.changeset/real-peaches-brush.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": patch +--- + +use provided message in Schema.TaggedError .toString diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index 236454ae62..7fd3e35443 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -6044,8 +6044,7 @@ export const TaggedClass = (identifier?: string) => {}, {} > => -{ - return makeClass({ + makeClass({ kind: "TaggedClass", identifier: identifier ?? tag, fields: extendFields({ _tag: literal(tag) }, fields), @@ -6053,7 +6052,6 @@ export const TaggedClass = (identifier?: string) => tag: { _tag: tag }, annotations }) -} /** * @category classes @@ -6076,13 +6074,25 @@ export const TaggedError = (identifier?: string) => Cause.YieldableError > => { + class Base extends Data.Error {} + ;(Base.prototype as any).name = tag return makeClass({ kind: "TaggedError", identifier: identifier ?? tag, fields: extendFields({ _tag: literal(tag) }, fields), - Base: Data.Error, + Base, tag: { _tag: tag }, - annotations + annotations, + toStringOverride(self) { + if (!(Predicate.isString(self.message) && self.message.length > 0)) { + return Pretty.make(self.constructor as any)(self) + } + let message = `${self._tag}: ${self.message}` + if (Predicate.isString(self.stack)) { + message = `${message}\n${self.stack.split("\n").slice(1).join("\n")}` + } + return message + } }) } @@ -6173,7 +6183,7 @@ const extendFields = (a: Struct.Fields, b: Struct.Fields): Struct.Fields => { return out } -const makeClass = ({ Base, annotations, fields, fromSchema, identifier, kind, tag }: { +const makeClass = ({ Base, annotations, fields, fromSchema, identifier, kind, tag, toStringOverride }: { kind: string identifier: string fields: Struct.Fields @@ -6181,6 +6191,7 @@ const makeClass = ({ Base, annotations, fields, fromSchema, identifier, kind, ta fromSchema?: Schema.Any | undefined tag?: { _tag: AST.LiteralValue } | undefined annotations?: Annotations.Schema | undefined + toStringOverride?: ((self: any) => string) | undefined }): any => { const classSymbol = Symbol.for(`@effect/schema/${kind}/${identifier}`) const schema = fromSchema ?? struct(fields) @@ -6207,7 +6218,7 @@ const makeClass = ({ Base, annotations, fields, fromSchema, identifier, kind, ta } toString() { - return Pretty.make(this.constructor as any)(this) + return toStringOverride !== undefined ? toStringOverride(this) : Pretty.make(this.constructor as any)(this) } static pipe() { @@ -6636,13 +6647,30 @@ function causeEncode(cause: Cause.Cause): CauseEncoded { } } -const causeDefectPretty: Schema = transform( +/** + * @category Cause transformations + * @since 1.0.0 + */ +export const causeDefectUnknown: $unknown = transform( unknown, unknown, - identity, + (u) => { + if (Predicate.isObject(u) && "message" in u && typeof u.message === "string") { + const err = new Error(u.message, { cause: u }) + if ("name" in u && typeof u.name === "string") { + err.name = u.name + } + err.stack = "stack" in u && typeof u.stack === "string" ? u.stack : "" + return err + } + return String(u) + }, (defect) => { - if (Predicate.isObject(defect)) { - return Cause.pretty(Cause.die(defect)) + if (defect instanceof Error) { + return { + name: defect.name, + message: defect.message + } } return String(defect) } @@ -6665,7 +6693,7 @@ export interface cause extends * @category Cause transformations * @since 1.0.0 */ -export const cause = ({ defect = causeDefectPretty, error }: { +export const cause = ({ defect = causeDefectUnknown, error }: { readonly error: E readonly defect?: Schema | undefined }): cause => { @@ -6817,7 +6845,7 @@ export interface exit extends * @since 1.0.0 */ export const exit = ( - { defect = causeDefectPretty, failure, success }: { + { defect = causeDefectUnknown, failure, success }: { readonly failure: E readonly success: A readonly defect?: Schema | undefined diff --git a/packages/schema/test/Cause/cause.test.ts b/packages/schema/test/Cause/cause.test.ts index 81ef1b0cf6..fc186c8df3 100644 --- a/packages/schema/test/Cause/cause.test.ts +++ b/packages/schema/test/Cause/cause.test.ts @@ -44,7 +44,7 @@ describe("Cause > cause", () => { _tag: "Die", defect: { stack: "fail", message: "error" } }, - Cause.die({ stack: "fail", message: "error" }) + Cause.die(new Error("error")) ) await Util.expectDecodeUnknownSuccess( schema, @@ -140,12 +140,13 @@ describe("Cause > cause", () => { let failWithStack = S.encodeSync(schema)(Cause.die(new Error("fail"))) assert(failWithStack._tag === "Die") - assert.include(failWithStack.defect, "Error: fail") - assert.include(failWithStack.defect, "cause.test.ts") + assert.deepStrictEqual(failWithStack.defect, { + name: "Error", + message: "fail" + }) failWithStack = S.encodeSync(schemaUnknown)(Cause.die(new Error("fail"))) assert(failWithStack._tag === "Die") - assert.strictEqual((failWithStack.defect as any).message, "fail") - assert.include((failWithStack.defect as any).stack, "cause.test.ts") + assert.strictEqual((failWithStack.defect as Error).message, "fail") }) }) diff --git a/packages/schema/test/Schema/Class.test.ts b/packages/schema/test/Schema/Class.test.ts index 3b20aa2de9..610aceb787 100644 --- a/packages/schema/test/Schema/Class.test.ts +++ b/packages/schema/test/Schema/Class.test.ts @@ -646,6 +646,24 @@ describe("Schema > Class APIs", () => { expect(err.id).toEqual(1) }) + it("TaggedError/message", () => { + class MyError extends S.TaggedError()("MyError", { + id: S.number + }) { + get message() { + return `bad id: ${this.id}` + } + } + + const err = new MyError({ id: 1 }) + + expect(String(err)).include(`MyError: bad id: 1`) + expect(String(err)).toContain("Class.test.ts:") + expect(err.stack).toContain("Class.test.ts:") + expect(err._tag).toEqual("MyError") + expect(err.id).toEqual(1) + }) + describe("TaggedRequest", () => { it("should expose the fields", () => { class TRA extends S.TaggedRequest()("TRA", S.string, S.number, {