-
Couldn't load subscription status.
- Fork 1.4k
Add *WithTrace variants for error handling combinators #3145
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little worried about the number of different error handling combinators we have at this point, though admittedly that is a cumulative problem and not solely the result of this PR.
The other thing that seems slightly questionable here is we are capturing a trace but it is possibly not the full trace.
If the use case is really just to log the traces, which I think it probably is considering that it is hard to do a lot else with traces and how long we have gone without having trace combinators, could we address this more fully and more minimally with the existing tapCause combinator? This would give access to the full cause and trace without unintentionally catching errors or defects, which it seems like would address the motivations for the original issue.
|
@adamgfraser just foo.catchSomeWithTrace {
case (e: BarError, trace) =>
log.warn(trace, s"Failed to foo with $e, falling back") *> recover
}An alternative to new combinators would be having a object Failure {
def unapply[E](cause: Cause[E]): Option[E] = cause.failureOption
// Or
def unapply[E](cause: Cause[E]): Option[(E, Option[ZTrace])] = cause.failureWithTraceOption
}so that you could write foo.catchSomeCause {
case c @ Failure(e: BarError) =>
log.warn(c, s"Failed to foo with $e, falling back") *> recover
case c @ Failure(e) =>
log.error(c, s"Failed to foo: $e") *> ZIO.halt(c)
}Not sure what is the proper trace to log here, is it |
|
@adamgfraser There is also a problem with replacing foo.catchAll {
case e: BarError => recover1: UIO[A]
case e: BazError => recover2: UIO[A]
}: UIO[A]and now want to add logging. We can't use foo.catchAllCause {
case c @ Failure(e: BarError) => log(c, ...) *> recover1: UIO[A]
case c @ Failure(e: BazError) => log(c, ...) *> recover2: UIO[A]
case c => ZIO.halt(c): IO[E, Nothing] // !!!
}: IO[E, A]Something like trait Failure[+E] {
def value: E
def fullCause: Cause[E]
}
def catchAllFailure[E1, A1 >: A](f: Failure[E] => IO[E1, A1]): IO[E1, A1] could help solve it |
|
@mvv You make good points. What if we do something like: trait ZIO[-R, +E, +A] {
final def withTrace: ZIO[R, (E, Option[ZTrace]), A]
}Basically, this "surfaces" the trace into the error channel. I am coming around to the view that the trace should be only the trace associated with the "primary" error. Then you could use all the normal error handling combinators to deal with errors and log the trace. |
|
@adamgfraser foo
.withTrace
.catchSome {
case ... => recover // Have to be IO[E1 >: (E, Option[ZTrace]), A]
}
.mapError(_._1) // Have to map it backAlso, now that I think about it, |
|
@mvv Yes, there is an inverse operation that submerges the trace back into the cause. Potentially we could bundle the two together into something like: /**
* Performs the specified effect with the trace of the "primary" cause
* surfaced to the error channel.
*/
def withTrace[R1, E1, A1](f: ZIO[R, (E, Option[ZTrace]), A] => ZIO[R1, E1, A1]): ZIO[R1, E1, A1] =
f(self.catchAllCause(_.failureWithTraceOrCause.fold(ZIO.fail(_), ZIO.halt(_))))That addresses your second point but not your first one since we still need our def catchSomeWithTrace[R1 <: R, E1 >: E, A1 >: A](pf: PartialFunction[(E, Option[ZTrace]), ZIO[R1, E1, A1]]): ZIO[R1, E1, A1] =
withTrace { effect =>
effect.catchAll(pf.applyOrElse(_, (e: (E, Option[ZTrace])) => ZIO.fail(e._1)))
} |
|
@adamgfraser So we are back to the original combinators, just with different implementation? Or do you want to skip foo.tapErrorWithTrace { case (e, trace) => log(trace, s"Got error: $e" }would become something like foo.withTrace(_.tapError { case (e, trace) => log(trace, s"Got error: $e") })Not much longer, but more nesting. We could also pick a less verbose name for |
|
@mvv Yes, thanks for bearing with me on this. I've been trying to figure out the "primitive" we were missing here that we could implement other things in terms of. You've sold me on needing some specialized combinators that can hopefully be implemented in terms of this. One thing we could do to cut down on the method length would be to do I'm fine with having some more specialized combinators. Maybe we keep |
|
@adamgfraser Made the rename. I kept |
|
@mvv Shall we add |
|
@adamgfraser Can't really think of a use case for accessing the trace in pure function. Same goes for |
|
Okay. I have a feeling we are going to have someone ask us for it at some point in the future but we can deal with that when the time comes. 😃 |
|
Thanks for contributing! |
Fixes #2323 by providing
*WithTracevariants of error handling combinators. TheCause.failureWithTraceOptionshould probably be extended to handle at leastTraced(Meta(...Meta(Fail(e), ...)), trace)cases, maybe more (doesTraced(Traced(Fail(e), trace2), trace1)make sense?). Would appreciate some input on this.