From 71a63360829aaba48f017d36cd9ed809ca1b6a1b Mon Sep 17 00:00:00 2001 From: Doron Somech Date: Tue, 21 Jan 2025 16:48:30 +0200 Subject: [PATCH 1/9] add support for catching failures only, without defects --- .../shared/src/test/scala/zio/ZIOSpec.scala | 71 +++++++++++++++++++ core/shared/src/main/scala/zio/Cause.scala | 7 ++ core/shared/src/main/scala/zio/ZIO.scala | 44 ++++++++++++ 3 files changed, 122 insertions(+) diff --git a/core-tests/shared/src/test/scala/zio/ZIOSpec.scala b/core-tests/shared/src/test/scala/zio/ZIOSpec.scala index 9594c1fad8fb..69da2969e569 100644 --- a/core-tests/shared/src/test/scala/zio/ZIOSpec.scala +++ b/core-tests/shared/src/test/scala/zio/ZIOSpec.scala @@ -307,6 +307,29 @@ object ZIOSpec extends ZIOBaseSpec { } yield assert(result)((equalTo(t))) } ) @@ zioTag(errors), + suite("catchAllFailure")( + test("recovers from all failures") { + val s = "division by zero" + val zio = ZIO.fail(new IllegalArgumentException(s)) + for { + result <- zio.catchAllFailure(e => ZIO.succeed(e.value.getMessage)) + } yield assert(result)(equalTo(s)) + }, + test("leaves defects") { + val t = new IllegalArgumentException("division by zero") + val zio = ZIO.attempt(true) *> ZIO.die(t) + for { + exit <- zio.catchAllFailure(e => ZIO.succeed(e.value.getMessage)).exit + } yield assert(exit)(dies(equalTo(t))) + }, + test("leaves values") { + val t = new IllegalArgumentException("division by zero") + val zio = ZIO.attempt(t) + for { + result <- zio.catchAllFailure(e => ZIO.succeed(e.value.getMessage)) + } yield assert(result)((equalTo(t))) + } + ) @@ zioTag(errors), suite("catchSomeCause")( test("catches matching cause") { ZIO.interrupt.catchSomeCause { @@ -363,6 +386,36 @@ object ZIOSpec extends ZIOBaseSpec { } yield assert(result)((equalTo(t))) } ) @@ zioTag(errors), + suite("catchSomeFailure")( + test("catches matching cause") { + ZIO + .fail("fail") + .catchSomeFailure { + case c if c.value == "fail" => ZIO.succeed(true) + } + .sandbox + .map( + assert(_)(isTrue) + ) + }, + test("fails if cause doesn't match") { + ZIO + .fail("no-match") + .catchSomeFailure { + case c if c.value == "fail" => ZIO.succeed(true) + } + .sandbox + .either + .map( + assert(_)(isLeft(equalTo(Cause.fail("no-match")))) + ) + }, + test("doesn't catch defects") { + for { + result <- (ZIO.attempt(42) *> ZIO.dieMessage("die")).catchSomeFailure(_ => ZIO.succeed(true)).exit + } yield assert(result)(dies(hasMessage(equalTo("die")))) + } + ) @@ zioTag(errors), suite("collect")( test("returns failure ignoring value") { for { @@ -3850,6 +3903,24 @@ object ZIOSpec extends ZIOBaseSpec { } yield assert(effect)(equalTo(42)) } ), + suite("tapFailure")( + test("doesn't peek at the defect of this effect") { + for { + ref <- Ref.make(false) + result <- (ZIO.attempt(42) *> ZIO.dieMessage("die")).tapFailure(_ => ref.set(true)).exit + effect <- ref.get + } yield assert(result)(dies(hasMessage(equalTo("die")))) && + assert(effect)(isFalse) + }, + test("effectually peeks at the failure of this effect") { + for { + ref <- Ref.make(false) + result <- ZIO.fail("fail").tapFailure(_ => ref.set(true)).exit + effect <- ref.get + } yield assert(result)(fails(equalTo("fail"))) && + assert(effect)(isTrue) + } + ), suite("tapSome")( test("is identity if the function doesn't match") { for { diff --git a/core/shared/src/main/scala/zio/Cause.scala b/core/shared/src/main/scala/zio/Cause.scala index 9c62a7c7fc83..dd24753925df 100644 --- a/core/shared/src/main/scala/zio/Cause.scala +++ b/core/shared/src/main/scala/zio/Cause.scala @@ -115,6 +115,13 @@ sealed abstract class Cause[+E] extends Product with Serializable { self => def failureOption: Option[E] = find { case Fail(e, _) => e } + /** + * Returns the `Cause.Fail[E]` associated with the first `Fail` in this + * `Cause` if one exists. + */ + def failureCauseOption: Option[Fail[E]] = + find { case f: Fail[E] => f } + /** * Returns the `E` associated with the first `Fail` in this `Cause` if one * exists, along with its (optional) trace. diff --git a/core/shared/src/main/scala/zio/ZIO.scala b/core/shared/src/main/scala/zio/ZIO.scala index b195ead4765b..347c918c00f2 100644 --- a/core/shared/src/main/scala/zio/ZIO.scala +++ b/core/shared/src/main/scala/zio/ZIO.scala @@ -329,6 +329,18 @@ sealed trait ZIO[-R, +E, +A] ): ZIO[R1, E1, A1] = catchSomeDefect { case t => h(t) } + /** + * Recovers from all failures with provided function. + * + * {{{ + * effect.catchAllFailure(_ => backup()) + * }}} + */ + final def catchAllFailure[R1 <: R, E1 >: E, A1 >: A](h: Cause.Fail[E] => ZIO[R1, E1, A1])(implicit + trace: Trace + ): ZIO[R1, E1, A1] = + catchSomeFailure { case t => h(t) } + /** * Recovers from all NonFatal Throwables. * @@ -412,6 +424,24 @@ sealed trait ZIO[-R, +E, +A] )(implicit trace: Trace): ZIO[R1, E1, A1] = unrefineWith(pf)(ZIO.failFn).catchAll(identity) + /** + * Recovers from some or all of the failure cases with provided cause. + * + * {{{ + * openFile("data.json").catchSomeFailure { + * case c if (c.interrupted) => openFile("backup.json") + * } + * }}} + */ + final def catchSomeFailure[R1 <: R, E1 >: E, A1 >: A]( + pf: PartialFunction[Cause.Fail[E], ZIO[R1, E1, A1]] + )(implicit trace: Trace): ZIO[R1, E1, A1] = { + def tryRescue(c: Cause.Fail[E]): ZIO[R1, E1, A1] = + pf.applyOrElse(c, (_: Cause.Fail[E]) => Exit.failCause(c)) + + self.foldCauseZIO(c => c.failureCauseOption.fold[ZIO[R1, E1, A1]](Exit.failCause(c))(tryRescue), ZIO.successFn) + } + /** * Returns an effect that succeeds with the cause of failure of this effect, * or `Cause.empty` if the effect did succeed. @@ -2111,6 +2141,20 @@ sealed trait ZIO[-R, +E, +A] ZIO.successFn ) + /** + * Returns an effect that effectfully "peeks" at the failure of this effect. + * {{{ + * readFile("data.json").tapError(logError(_)) + * }}} + */ + final def tapFailure[R1 <: R, E1 >: E]( + f: Cause.Fail[E] => ZIO[R1, E1, Any] + )(implicit ev: CanFail[E], trace: Trace): ZIO[R1, E1, A] = + self.foldCauseZIO( + c => c.failureCauseOption.fold[ZIO[R1, E1, Nothing]](Exit.failCause(c))(f(_) *> Exit.failCause(c)), + ZIO.successFn + ) + /** * Returns an effect that effectfully "peeks" at the success of this effect. * If the partial function isn't defined at the input, the result is From 573ffba626d4776d6b1efa6d96bebb39c6cdee25 Mon Sep 17 00:00:00 2001 From: Doron Somech Date: Tue, 21 Jan 2025 17:45:49 +0200 Subject: [PATCH 2/9] fix scala 2.12 --- core-tests/shared/src/test/scala/zio/ZIOSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core-tests/shared/src/test/scala/zio/ZIOSpec.scala b/core-tests/shared/src/test/scala/zio/ZIOSpec.scala index 69da2969e569..e6a507b5ddec 100644 --- a/core-tests/shared/src/test/scala/zio/ZIOSpec.scala +++ b/core-tests/shared/src/test/scala/zio/ZIOSpec.scala @@ -412,7 +412,7 @@ object ZIOSpec extends ZIOBaseSpec { }, test("doesn't catch defects") { for { - result <- (ZIO.attempt(42) *> ZIO.dieMessage("die")).catchSomeFailure(_ => ZIO.succeed(true)).exit + result <- (ZIO.attempt(42) *> ZIO.dieMessage("die")).catchSomeFailure { case _ => ZIO.succeed(true) }.exit } yield assert(result)(dies(hasMessage(equalTo("die")))) } ) @@ zioTag(errors), From 796e7161b57f051e6d67c5b20ee0c35df4a730de Mon Sep 17 00:00:00 2001 From: Doron Somech Date: Wed, 22 Jan 2025 15:23:01 +0200 Subject: [PATCH 3/9] addressing PR comments --- .../shared/src/test/scala/zio/ZIOSpec.scala | 10 +++---- core/shared/src/main/scala/zio/Cause.scala | 29 ++++++++++++++----- core/shared/src/main/scala/zio/ZIO.scala | 20 ++++++------- 3 files changed, 37 insertions(+), 22 deletions(-) diff --git a/core-tests/shared/src/test/scala/zio/ZIOSpec.scala b/core-tests/shared/src/test/scala/zio/ZIOSpec.scala index e6a507b5ddec..8ab8c3414afa 100644 --- a/core-tests/shared/src/test/scala/zio/ZIOSpec.scala +++ b/core-tests/shared/src/test/scala/zio/ZIOSpec.scala @@ -312,21 +312,21 @@ object ZIOSpec extends ZIOBaseSpec { val s = "division by zero" val zio = ZIO.fail(new IllegalArgumentException(s)) for { - result <- zio.catchAllFailure(e => ZIO.succeed(e.value.getMessage)) + result <- zio.catchAllFailure(e => ZIO.succeed(e.failureOption.get.getMessage)) } yield assert(result)(equalTo(s)) }, test("leaves defects") { val t = new IllegalArgumentException("division by zero") val zio = ZIO.attempt(true) *> ZIO.die(t) for { - exit <- zio.catchAllFailure(e => ZIO.succeed(e.value.getMessage)).exit + exit <- zio.catchAllFailure(e => ZIO.succeed(e.failureOption.get.getMessage)).exit } yield assert(exit)(dies(equalTo(t))) }, test("leaves values") { val t = new IllegalArgumentException("division by zero") val zio = ZIO.attempt(t) for { - result <- zio.catchAllFailure(e => ZIO.succeed(e.value.getMessage)) + result <- zio.catchAllFailure(e => ZIO.succeed(e.failureOption.get.getMessage)) } yield assert(result)((equalTo(t))) } ) @@ zioTag(errors), @@ -391,7 +391,7 @@ object ZIOSpec extends ZIOBaseSpec { ZIO .fail("fail") .catchSomeFailure { - case c if c.value == "fail" => ZIO.succeed(true) + case c if c.failureOption.get == "fail" => ZIO.succeed(true) } .sandbox .map( @@ -402,7 +402,7 @@ object ZIOSpec extends ZIOBaseSpec { ZIO .fail("no-match") .catchSomeFailure { - case c if c.value == "fail" => ZIO.succeed(true) + case c if c.failureOption.get == "fail" => ZIO.succeed(true) } .sandbox .either diff --git a/core/shared/src/main/scala/zio/Cause.scala b/core/shared/src/main/scala/zio/Cause.scala index dd24753925df..83c7df66eda9 100644 --- a/core/shared/src/main/scala/zio/Cause.scala +++ b/core/shared/src/main/scala/zio/Cause.scala @@ -115,13 +115,6 @@ sealed abstract class Cause[+E] extends Product with Serializable { self => def failureOption: Option[E] = find { case Fail(e, _) => e } - /** - * Returns the `Cause.Fail[E]` associated with the first `Fail` in this - * `Cause` if one exists. - */ - def failureCauseOption: Option[Fail[E]] = - find { case f: Fail[E] => f } - /** * Returns the `E` associated with the first `Fail` in this `Cause` if one * exists, along with its (optional) trace. @@ -419,6 +412,28 @@ sealed abstract class Cause[+E] extends Product with Serializable { self => (causeOption, stackless) => causeOption.map(Stackless(_, stackless)) ) + def keepFailures: Option[Cause[E]] = + foldLog[Option[Cause[E]]]( + None, + (e, trace, spans, annotations) => Some(Fail(e, trace, spans, annotations)), + (_, _, _, _) => None, + (_, _, _, _) => None + )( + { + case (Some(l), Some(r)) => Some(Then(l, r)) + case (Some(l), None) => Some(l) + case (None, Some(r)) => Some(r) + case (None, None) => None + }, + { + case (Some(l), Some(r)) => Some(Both(l, r)) + case (Some(l), None) => Some(l) + case (None, Some(r)) => Some(r) + case (None, None) => None + }, + (causeOption, stackless) => causeOption.map(Stackless(_, stackless)) + ) + /** * Linearizes this cause to a set of parallel causes where each parallel cause * contains a linear sequence of failures. diff --git a/core/shared/src/main/scala/zio/ZIO.scala b/core/shared/src/main/scala/zio/ZIO.scala index 347c918c00f2..9c53d44aea04 100644 --- a/core/shared/src/main/scala/zio/ZIO.scala +++ b/core/shared/src/main/scala/zio/ZIO.scala @@ -336,9 +336,9 @@ sealed trait ZIO[-R, +E, +A] * effect.catchAllFailure(_ => backup()) * }}} */ - final def catchAllFailure[R1 <: R, E1 >: E, A1 >: A](h: Cause.Fail[E] => ZIO[R1, E1, A1])(implicit - trace: Trace - ): ZIO[R1, E1, A1] = + final def catchAllFailure[R1 <: R, E1 >: E, A1 >: A]( + h: Cause[E] => ZIO[R1, E1, A1] + )(implicit ev: CanFail[E], trace: Trace): ZIO[R1, E1, A1] = catchSomeFailure { case t => h(t) } /** @@ -434,12 +434,12 @@ sealed trait ZIO[-R, +E, +A] * }}} */ final def catchSomeFailure[R1 <: R, E1 >: E, A1 >: A]( - pf: PartialFunction[Cause.Fail[E], ZIO[R1, E1, A1]] - )(implicit trace: Trace): ZIO[R1, E1, A1] = { - def tryRescue(c: Cause.Fail[E]): ZIO[R1, E1, A1] = - pf.applyOrElse(c, (_: Cause.Fail[E]) => Exit.failCause(c)) + pf: PartialFunction[Cause[E], ZIO[R1, E1, A1]] + )(implicit ev: CanFail[E], trace: Trace): ZIO[R1, E1, A1] = { + def tryRescue(c: Cause[E]): ZIO[R1, E1, A1] = + pf.applyOrElse(c, (_: Cause[E]) => Exit.failCause(c)) - self.foldCauseZIO(c => c.failureCauseOption.fold[ZIO[R1, E1, A1]](Exit.failCause(c))(tryRescue), ZIO.successFn) + self.foldCauseZIO(c => c.keepFailures.fold[ZIO[R1, E1, A1]](Exit.failCause(c))(tryRescue), ZIO.successFn) } /** @@ -2148,10 +2148,10 @@ sealed trait ZIO[-R, +E, +A] * }}} */ final def tapFailure[R1 <: R, E1 >: E]( - f: Cause.Fail[E] => ZIO[R1, E1, Any] + f: Cause[E] => ZIO[R1, E1, Any] )(implicit ev: CanFail[E], trace: Trace): ZIO[R1, E1, A] = self.foldCauseZIO( - c => c.failureCauseOption.fold[ZIO[R1, E1, Nothing]](Exit.failCause(c))(f(_) *> Exit.failCause(c)), + c => c.keepFailures.fold[ZIO[R1, E1, Nothing]](Exit.failCause(c))(f(_) *> Exit.failCause(c)), ZIO.successFn ) From c3e74b90fefaf89558216dba982cfb511a274bde Mon Sep 17 00:00:00 2001 From: Doron Somech Date: Wed, 22 Jan 2025 16:19:07 +0200 Subject: [PATCH 4/9] only recover from failures --- core/shared/src/main/scala/zio/Cause.scala | 14 ++++++++++++++ core/shared/src/main/scala/zio/ZIO.scala | 9 ++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/core/shared/src/main/scala/zio/Cause.scala b/core/shared/src/main/scala/zio/Cause.scala index 83c7df66eda9..b332c17021ec 100644 --- a/core/shared/src/main/scala/zio/Cause.scala +++ b/core/shared/src/main/scala/zio/Cause.scala @@ -364,6 +364,9 @@ sealed abstract class Cause[+E] extends Product with Serializable { self => final def isFailure: Boolean = failureOption.isDefined + final def isFailureOnly: Boolean = + foldContext(())(Folder.IsFailureOnly) + /** * Determines if the `Cause` contains an interruption. */ @@ -844,6 +847,17 @@ object Cause extends Serializable { def stacklessCase(context: Any, value: Boolean, stackless: Boolean): Boolean = value } + case object IsFailureOnly extends Folder[Any, Any, Boolean] { + def empty(context: Any): Boolean = true + def failCase(context: Any, error: Any, stackTrace: StackTrace): Boolean = true + def dieCase(context: Any, t: Throwable, stackTrace: StackTrace): Boolean = false + def interruptCase(context: Any, fiberId: FiberId, stackTrace: StackTrace): Boolean = false + + def bothCase(context: Any, left: Boolean, right: Boolean): Boolean = left && right + def thenCase(context: Any, left: Boolean, right: Boolean): Boolean = left && right + def stacklessCase(context: Any, value: Boolean, stackless: Boolean): Boolean = value + } + final case class Filter[E](p: Cause[E] => Boolean) extends Folder[Any, E, Cause[E]] { def empty(context: Any): Cause[E] = Cause.empty diff --git a/core/shared/src/main/scala/zio/ZIO.scala b/core/shared/src/main/scala/zio/ZIO.scala index 9c53d44aea04..bbc7486a7b93 100644 --- a/core/shared/src/main/scala/zio/ZIO.scala +++ b/core/shared/src/main/scala/zio/ZIO.scala @@ -437,9 +437,12 @@ sealed trait ZIO[-R, +E, +A] pf: PartialFunction[Cause[E], ZIO[R1, E1, A1]] )(implicit ev: CanFail[E], trace: Trace): ZIO[R1, E1, A1] = { def tryRescue(c: Cause[E]): ZIO[R1, E1, A1] = - pf.applyOrElse(c, (_: Cause[E]) => Exit.failCause(c)) - - self.foldCauseZIO(c => c.keepFailures.fold[ZIO[R1, E1, A1]](Exit.failCause(c))(tryRescue), ZIO.successFn) + if (c.isFailureOnly) { + pf.applyOrElse(c, (_: Cause[E]) => Exit.failCause(c)) + } else { + Exit.failCause(c) + } + self.foldCauseZIO(tryRescue, ZIO.successFn) } /** From 2a0fc8fa7bfa05b2f664264134010f182b4949f5 Mon Sep 17 00:00:00 2001 From: Doron Somech Date: Sun, 26 Jan 2025 09:03:33 +0200 Subject: [PATCH 5/9] improve documentation --- core/shared/src/main/scala/zio/ZIO.scala | 38 ++++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/core/shared/src/main/scala/zio/ZIO.scala b/core/shared/src/main/scala/zio/ZIO.scala index bbc7486a7b93..3ae71dc78a7d 100644 --- a/core/shared/src/main/scala/zio/ZIO.scala +++ b/core/shared/src/main/scala/zio/ZIO.scala @@ -297,12 +297,17 @@ sealed trait ZIO[-R, +E, +A] self.foldTraceZIO[R1, E2, A1](h, ZIO.successFn) /** - * Recovers from all errors with provided Cause. + * Recovers from all errors and defects with provided Cause. * * {{{ * openFile("config.json").catchAllCause(_ => ZIO.succeed(defaultConfig)) * }}} * + * '''WARNING''': There is no sensible way to recover from defects. This + * method should be used only at the boundary between ZIO and an external + * system, to transmit information on a defect for diagnostic or explanatory + * purposes. Consider using `catchAll` or `catchAllFailure` instead. + * * @see * [[absorb]], [[sandbox]], [[mapErrorCause]] - other functions that can * recover from defects @@ -330,10 +335,15 @@ sealed trait ZIO[-R, +E, +A] catchSomeDefect { case t => h(t) } /** - * Recovers from all failures with provided function. + * Recovers from all failures with provided function. Equivalent to `catchAll` + * but with the provided Cause. If you only care about the error, use + * `catchAll` instead. + * + * This method doesn't recover from defects. If you need to recover from + * defects, use `catchAllCause` instead. * * {{{ - * effect.catchAllFailure(_ => backup()) + * effect.catchAllFailure(c => ZIO.logErrorCause("failure", c)) * }}} */ final def catchAllFailure[R1 <: R, E1 >: E, A1 >: A]( @@ -388,13 +398,18 @@ sealed trait ZIO[-R, +E, +A] } /** - * Recovers from some or all of the error cases with provided cause. + * Recovers from some or all of the error or defect cases with provided cause. * * {{{ * openFile("data.json").catchSomeCause { * case c if (c.interrupted) => openFile("backup.json") * } * }}} + * + * '''WARNING''': There is no sensible way to recover from defects. This + * method should be used only at the boundary between ZIO and an external + * system, to transmit information on a defect for diagnostic or explanatory + * purposes. Consider using `catchSomeFailure` instead. */ final def catchSomeCause[R1 <: R, E1 >: E, A1 >: A]( pf: PartialFunction[Cause[E], ZIO[R1, E1, A1]] @@ -427,9 +442,12 @@ sealed trait ZIO[-R, +E, +A] /** * Recovers from some or all of the failure cases with provided cause. * + * This method only recovers from failures. If you need to recover from + * defects as well, use `catchSomeCause` or `catchSomeDefect` instead. + * * {{{ - * openFile("data.json").catchSomeFailure { - * case c if (c.interrupted) => openFile("backup.json") + * effect.catchSomeFailure { + * case _: FileNotFoundException => createFile() * } * }}} */ @@ -2124,6 +2142,10 @@ sealed trait ZIO[-R, +E, +A] /** * Returns an effect that effectually "peeks" at the cause of the failure of * this effect. + * + * This method "peeks" at both the failure and defect of this effect. If you + * only need to "peek" at the failure, use `tapFailure` instead. + * * {{{ * readFile("data.json").tapErrorCause(logCause(_)) * }}} @@ -2146,6 +2168,10 @@ sealed trait ZIO[-R, +E, +A] /** * Returns an effect that effectfully "peeks" at the failure of this effect. + * + * This method only "peeks" at the failure of this effect. If you need to + * "peek" at defects as well, use `tapErrorCause` or `tapDefect` instead. + * * {{{ * readFile("data.json").tapError(logError(_)) * }}} From a18a1e11d7e8158697448d6b8d2b6a8992faa732 Mon Sep 17 00:00:00 2001 From: Doron Somech Date: Wed, 26 Mar 2025 12:45:49 +0200 Subject: [PATCH 6/9] apply new method names --- .../shared/src/test/scala/zio/ZIOSpec.scala | 14 ++++++++------ core/shared/src/main/scala/zio/ZIO.scala | 18 +++++++++--------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/core-tests/shared/src/test/scala/zio/ZIOSpec.scala b/core-tests/shared/src/test/scala/zio/ZIOSpec.scala index 79f3016ad44f..45c1392277e0 100644 --- a/core-tests/shared/src/test/scala/zio/ZIOSpec.scala +++ b/core-tests/shared/src/test/scala/zio/ZIOSpec.scala @@ -312,7 +312,7 @@ object ZIOSpec extends ZIOBaseSpec { val s = "division by zero" val zio = ZIO.fail(new IllegalArgumentException(s)) for { - result <- zio.catchAllFailure(e => ZIO.succeed(e.failureOption.get.getMessage)) + result <- zio.catchFailureCause(e => ZIO.succeed(e.failureOption.get.getMessage)) } yield assert(result)(equalTo(s)) }, test("leaves defects") { @@ -390,7 +390,7 @@ object ZIOSpec extends ZIOBaseSpec { test("catches matching cause") { ZIO .fail("fail") - .catchSomeFailure { + .catchSomeFailureCause { case c if c.failureOption.get == "fail" => ZIO.succeed(true) } .sandbox @@ -401,7 +401,7 @@ object ZIOSpec extends ZIOBaseSpec { test("fails if cause doesn't match") { ZIO .fail("no-match") - .catchSomeFailure { + .catchSomeFailureCause { case c if c.failureOption.get == "fail" => ZIO.succeed(true) } .sandbox @@ -412,7 +412,9 @@ object ZIOSpec extends ZIOBaseSpec { }, test("doesn't catch defects") { for { - result <- (ZIO.attempt(42) *> ZIO.dieMessage("die")).catchSomeFailure { case _ => ZIO.succeed(true) }.exit + result <- (ZIO.attempt(42) *> ZIO.dieMessage("die")).catchSomeFailureCause { case _ => + ZIO.succeed(true) + }.exit } yield assert(result)(dies(hasMessage(equalTo("die")))) } ) @@ zioTag(errors), @@ -3923,7 +3925,7 @@ object ZIOSpec extends ZIOBaseSpec { test("doesn't peek at the defect of this effect") { for { ref <- Ref.make(false) - result <- (ZIO.attempt(42) *> ZIO.dieMessage("die")).tapFailure(_ => ref.set(true)).exit + result <- (ZIO.attempt(42) *> ZIO.dieMessage("die")).tapFailureCause(_ => ref.set(true)).exit effect <- ref.get } yield assert(result)(dies(hasMessage(equalTo("die")))) && assert(effect)(isFalse) @@ -3931,7 +3933,7 @@ object ZIOSpec extends ZIOBaseSpec { test("effectually peeks at the failure of this effect") { for { ref <- Ref.make(false) - result <- ZIO.fail("fail").tapFailure(_ => ref.set(true)).exit + result <- ZIO.fail("fail").tapFailureCause(_ => ref.set(true)).exit effect <- ref.get } yield assert(result)(fails(equalTo("fail"))) && assert(effect)(isTrue) diff --git a/core/shared/src/main/scala/zio/ZIO.scala b/core/shared/src/main/scala/zio/ZIO.scala index 30938bb590e9..c6401f26421f 100644 --- a/core/shared/src/main/scala/zio/ZIO.scala +++ b/core/shared/src/main/scala/zio/ZIO.scala @@ -306,7 +306,7 @@ sealed trait ZIO[-R, +E, +A] * '''WARNING''': There is no sensible way to recover from defects. This * method should be used only at the boundary between ZIO and an external * system, to transmit information on a defect for diagnostic or explanatory - * purposes. Consider using `catchAll` or `catchAllFailure` instead. + * purposes. Consider using `catchAll` or `catchFailureCause` instead. * * @see * [[absorb]], [[sandbox]], [[mapErrorCause]] - other functions that can @@ -343,13 +343,13 @@ sealed trait ZIO[-R, +E, +A] * defects, use `catchAllCause` instead. * * {{{ - * effect.catchAllFailure(c => ZIO.logErrorCause("failure", c)) + * effect.catchFailureCause(c => ZIO.logErrorCause("failure", c)) * }}} */ - final def catchAllFailure[R1 <: R, E1 >: E, A1 >: A]( + final def catchFailureCause[R1 <: R, E1 >: E, A1 >: A]( h: Cause[E] => ZIO[R1, E1, A1] )(implicit ev: CanFail[E], trace: Trace): ZIO[R1, E1, A1] = - catchSomeFailure { case t => h(t) } + catchSomeFailureCause { case t => h(t) } /** * Recovers from all NonFatal Throwables. @@ -409,7 +409,7 @@ sealed trait ZIO[-R, +E, +A] * '''WARNING''': There is no sensible way to recover from defects. This * method should be used only at the boundary between ZIO and an external * system, to transmit information on a defect for diagnostic or explanatory - * purposes. Consider using `catchSomeFailure` instead. + * purposes. Consider using `catchSomeFailureCause` instead. */ final def catchSomeCause[R1 <: R, E1 >: E, A1 >: A]( pf: PartialFunction[Cause[E], ZIO[R1, E1, A1]] @@ -446,12 +446,12 @@ sealed trait ZIO[-R, +E, +A] * defects as well, use `catchSomeCause` or `catchSomeDefect` instead. * * {{{ - * effect.catchSomeFailure { + * effect.catchSomeFailureCause { * case _: FileNotFoundException => createFile() * } * }}} */ - final def catchSomeFailure[R1 <: R, E1 >: E, A1 >: A]( + final def catchSomeFailureCause[R1 <: R, E1 >: E, A1 >: A]( pf: PartialFunction[Cause[E], ZIO[R1, E1, A1]] )(implicit ev: CanFail[E], trace: Trace): ZIO[R1, E1, A1] = { def tryRescue(c: Cause[E]): ZIO[R1, E1, A1] = @@ -2154,7 +2154,7 @@ sealed trait ZIO[-R, +E, +A] * this effect. * * This method "peeks" at both the failure and defect of this effect. If you - * only need to "peek" at the failure, use `tapFailure` instead. + * only need to "peek" at the failure, use `tapFailureCause` instead. * * {{{ * readFile("data.json").tapErrorCause(logCause(_)) @@ -2186,7 +2186,7 @@ sealed trait ZIO[-R, +E, +A] * readFile("data.json").tapError(logError(_)) * }}} */ - final def tapFailure[R1 <: R, E1 >: E]( + final def tapFailureCause[R1 <: R, E1 >: E]( f: Cause[E] => ZIO[R1, E1, Any] )(implicit ev: CanFail[E], trace: Trace): ZIO[R1, E1, A] = self.foldCauseZIO( From ff10117df7073655bd93900f765e91b2cb4cc7b4 Mon Sep 17 00:00:00 2001 From: Doron Somech Date: Wed, 26 Mar 2025 12:57:07 +0200 Subject: [PATCH 7/9] fix compilation error --- core-tests/shared/src/test/scala/zio/ZIOSpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core-tests/shared/src/test/scala/zio/ZIOSpec.scala b/core-tests/shared/src/test/scala/zio/ZIOSpec.scala index 45c1392277e0..8c467778255c 100644 --- a/core-tests/shared/src/test/scala/zio/ZIOSpec.scala +++ b/core-tests/shared/src/test/scala/zio/ZIOSpec.scala @@ -319,14 +319,14 @@ object ZIOSpec extends ZIOBaseSpec { val t = new IllegalArgumentException("division by zero") val zio = ZIO.attempt(true) *> ZIO.die(t) for { - exit <- zio.catchAllFailure(e => ZIO.succeed(e.failureOption.get.getMessage)).exit + exit <- zio.catchFailureCause(e => ZIO.succeed(e.failureOption.get.getMessage)).exit } yield assert(exit)(dies(equalTo(t))) }, test("leaves values") { val t = new IllegalArgumentException("division by zero") val zio = ZIO.attempt(t) for { - result <- zio.catchAllFailure(e => ZIO.succeed(e.failureOption.get.getMessage)) + result <- zio.catchFailureCause(e => ZIO.succeed(e.failureOption.get.getMessage)) } yield assert(result)((equalTo(t))) } ) @@ zioTag(errors), From 5fcc9eefc67a6b0f9148678ca38963c7e43b8a1d Mon Sep 17 00:00:00 2001 From: Doron Somech Date: Thu, 27 Mar 2025 15:51:55 +0200 Subject: [PATCH 8/9] change type to Cause.Fail --- core/shared/src/main/scala/zio/ZIO.scala | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/core/shared/src/main/scala/zio/ZIO.scala b/core/shared/src/main/scala/zio/ZIO.scala index c6401f26421f..3d147cd32151 100644 --- a/core/shared/src/main/scala/zio/ZIO.scala +++ b/core/shared/src/main/scala/zio/ZIO.scala @@ -347,7 +347,7 @@ sealed trait ZIO[-R, +E, +A] * }}} */ final def catchFailureCause[R1 <: R, E1 >: E, A1 >: A]( - h: Cause[E] => ZIO[R1, E1, A1] + h: Cause.Fail[E] => ZIO[R1, E1, A1] )(implicit ev: CanFail[E], trace: Trace): ZIO[R1, E1, A1] = catchSomeFailureCause { case t => h(t) } @@ -452,11 +452,14 @@ sealed trait ZIO[-R, +E, +A] * }}} */ final def catchSomeFailureCause[R1 <: R, E1 >: E, A1 >: A]( - pf: PartialFunction[Cause[E], ZIO[R1, E1, A1]] + pf: PartialFunction[Cause.Fail[E], ZIO[R1, E1, A1]] )(implicit ev: CanFail[E], trace: Trace): ZIO[R1, E1, A1] = { def tryRescue(c: Cause[E]): ZIO[R1, E1, A1] = if (c.isFailureOnly) { - pf.applyOrElse(c, (_: Cause[E]) => Exit.failCause(c)) + c.find { case f: Cause.Fail[E] => f } match { + case Some(f) => pf.applyOrElse(f, (_: Cause.Fail[E]) => Exit.failCause(c)) + case None => Exit.failCause(c) + } } else { Exit.failCause(c) } @@ -2187,10 +2190,12 @@ sealed trait ZIO[-R, +E, +A] * }}} */ final def tapFailureCause[R1 <: R, E1 >: E]( - f: Cause[E] => ZIO[R1, E1, Any] + f: Cause.Fail[E] => ZIO[R1, E1, Any] )(implicit ev: CanFail[E], trace: Trace): ZIO[R1, E1, A] = self.foldCauseZIO( - c => c.keepFailures.fold[ZIO[R1, E1, Nothing]](Exit.failCause(c))(f(_) *> Exit.failCause(c)), + c => + c.find { case failure: Cause.Fail[E] => failure } + .fold[ZIO[R1, E1, Nothing]](Exit.failCause(c))(f(_) *> Exit.failCause(c)), ZIO.successFn ) From 769da161f4bf73bc2a0ac66fc20065c2ea273a3f Mon Sep 17 00:00:00 2001 From: Doron Somech Date: Tue, 1 Apr 2025 14:56:58 +0300 Subject: [PATCH 9/9] fix catchFailureCause return type --- core/shared/src/main/scala/zio/ZIO.scala | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/core/shared/src/main/scala/zio/ZIO.scala b/core/shared/src/main/scala/zio/ZIO.scala index 3d147cd32151..7b8c4acf8cda 100644 --- a/core/shared/src/main/scala/zio/ZIO.scala +++ b/core/shared/src/main/scala/zio/ZIO.scala @@ -28,6 +28,13 @@ import scala.concurrent.ExecutionContext import scala.reflect.ClassTag import scala.util.control.NoStackTrace import izumi.reflect.macrortti.LightTypeTag +import zio.Cause.Empty +import zio.Cause.Then +import zio.Cause.Interrupt +import zio.Cause.Die +import zio.Cause.Stackless +import zio.Cause.Fail +import zio.Cause.Both /** * A `ZIO[R, E, A]` value is an immutable value (called an "effect") that @@ -346,10 +353,15 @@ sealed trait ZIO[-R, +E, +A] * effect.catchFailureCause(c => ZIO.logErrorCause("failure", c)) * }}} */ - final def catchFailureCause[R1 <: R, E1 >: E, A1 >: A]( - h: Cause.Fail[E] => ZIO[R1, E1, A1] - )(implicit ev: CanFail[E], trace: Trace): ZIO[R1, E1, A1] = - catchSomeFailureCause { case t => h(t) } + final def catchFailureCause[R1 <: R, E2, A1 >: A]( + h: Cause.Fail[E] => ZIO[R1, E2, A1] + )(implicit ev: CanFail[E], trace: Trace): ZIO[R1, E2, A1] = + self.foldTraceZIO[R1, E2, A1]( + { case (e, stackTrace) => + h(Cause.Fail(e, stackTrace)) + }, + ZIO.successFn + ) /** * Recovers from all NonFatal Throwables.