Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@mvv
Copy link
Contributor

@mvv mvv commented Mar 15, 2020

Fixes #2323 by providing *WithTrace variants of error handling combinators. The Cause.failureWithTraceOption should probably be extended to handle at least Traced(Meta(...Meta(Fail(e), ...)), trace) cases, maybe more (does Traced(Traced(Fail(e), trace2), trace1) make sense?). Would appreciate some input on this.

neko-kai
neko-kai previously approved these changes Mar 15, 2020
Copy link
Member

@neko-kai neko-kai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

@CLAassistant
Copy link

CLAassistant commented Mar 20, 2020

CLA assistant check
All committers have signed the CLA.

@neko-kai neko-kai requested a review from adamgfraser March 26, 2020 17:08
Copy link
Contributor

@adamgfraser adamgfraser left a 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.

@mvv
Copy link
Contributor Author

mvv commented Mar 28, 2020

@adamgfraser just tapCause won't do, because trace dumping is often conditional and tied to recovery, e.g.

  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 Cause extractor like

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 c.traces or the trace tied to the e we have extracted (in that case the second Failure.unapply variant should be used)?

@mvv
Copy link
Contributor Author

mvv commented Mar 28, 2020

@adamgfraser There is also a problem with replacing catchAll with catchAllCause. Let's say we have a

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 catchSomeCause, because the resulting expression won't have the desired UIO[A] type, but the same goes for catchAllCause with the extra fallback case:

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

@adamgfraser
Copy link
Contributor

@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.

@mvv
Copy link
Contributor Author

mvv commented Mar 29, 2020

@adamgfraser withTrace is clunky vs catchSome:

foo
  .withTrace
  .catchSome {
    case ... => recover // Have to be IO[E1 >: (E, Option[ZTrace]), A]
  }
  .mapError(_._1) // Have to map it back

Also, now that I think about it, Tuple2[E, Option[ZTrace]] is kinda awkward to refail/rehalt without loss of info.

@adamgfraser
Copy link
Contributor

@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 f to return an E instead of a (E, Option[ZTrace]) which doesn't work great with catchSome since we need a fallback to convert the unmatched cases back to an E. We could address that by implementing some more specialized combinators in terms of this. For example:

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)))
  }

@mvv
Copy link
Contributor Author

mvv commented Mar 30, 2020

@adamgfraser So we are back to the original combinators, just with different implementation? Or do you want to skip catchAllWithTrace/tapErrorWithTrace/foldWithTrace and just have withTrace and catchSomeWithTrace? I don't think those two deliver the same ergonomics, e.g.

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 tapErrorWithTrace, say, tapFailure, which would further increase the readability.

@adamgfraser
Copy link
Contributor

@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 Trace instead of WithTrace. So like it would be catchSomeTrace.

I'm fine with having some more specialized combinators. Maybe we keep catchAllTrace, catchSomeTrace, and tapErrorTrace and drop the foldM variant?

@mvv
Copy link
Contributor Author

mvv commented Mar 31, 2020

@adamgfraser Made the rename. I kept foldTraceM, because I personally use foldM quite a bit (mainly in Pulls, but in general to avoid the extra allocation of either.flatMap). I could write something like foo.catchAllTrace(ZIO.fail).foldM(...), but that's wordy.

@adamgfraser
Copy link
Contributor

@mvv Shall we add foldTrace for consistency with foldTraceM?

@mvv
Copy link
Contributor Author

mvv commented Mar 31, 2020

@adamgfraser Can't really think of a use case for accessing the trace in pure function. Same goes for mapError, for example. Some combinators work with errors as values and others work with them as failures, that's my mental model for this. flatMapError and forkWithErrorHandler seem to be in the latter category, but they are rarely used.

@adamgfraser
Copy link
Contributor

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. 😃

@adamgfraser adamgfraser merged commit 6eeab12 into zio:master Mar 31, 2020
@adamgfraser
Copy link
Contributor

Thanks for contributing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Error traces are not accessible via standard combinators

4 participants