Reputation: 1163
We have some legacy code in our codebase that is eventually going to be refactored to use Validated and Either from Cats library. This is because Validated does not use fail-fast mechanics. The unrefactored code uses fail-fast mechanics of Try monad.
Since the refactoring hasn't happened yet, I am doing a kludgy hack to get around the fact that the Try monad is fail-fast. I am having trouble implementing it however.
I basically have a list of type Try[T] that is guaranteed to all be Failures.
I am trying to aggregate all of the error messages of all the Failures into a single Failure.
Here is the function I am refactoring:
private def extractTry[T](xs: IndexedSeq[Try[T]]): Try[IndexedSeq[T]] = {
val failures = xs.collect { case Failure(ex) => Failure(ex) }
if (failures.size > 0) failures.head
else Success(xs.map(_.get))
}
Instead of failures.head in the second line of the method, I want to aggregate all the Failures.
So something like
if (failures.size > 0) failures.foldLeft(Failure(new IllegalArgumentException(""))){case (Failure(acc), Failure(e)) => Failure(new IllegalArgumentException(acc.getMessage + e.getMessage))}
The only thing I don't like about this implementation is that I would like each step of fold not to use IllegalArgumentException, but to use the new element's exception type. So the idea is to keep the exception type of the last element in failures, and not to use an arbitrary exception type.
We are planning to eventually use Either[Throwable, T] in place of Try and will probably run into the exact same problem there when we try to aggregate errors. We want to keep the exception type and not assign an arbitrary one like IllegalArgumentException. So this problem is going to have to be solved sooner or later, and I would prefer that it be sooner.
Does anyone have any suggestions? Any help would be appreciated.
Upvotes: 3
Views: 631
Reputation: 1163
Mario wrote up a response that I think deserves being the accepted answer because of its thoroughness. It was while reading his answer that I stumbled upon another solution that requires less code change, but still gets the job done.
The answer is to pattern match on the exception type, which seems very obvious in retrospect.
if (failures.size > 0) failures.foldLeft(Failure(new IllegalArgumentException(""))){case (Failure(acc), Failure(e)) =>
val message = acc.getMessage + e.getMessage
e match {
case ex: IllegalArgumentException => Failure(new IllegalArgumentException(message))
case ex: NoSuchElementException => Failure(new NoSuchElementException(message))
}
}
Upvotes: 0
Reputation: 48410
Ideally, we would follow suggestion by @Luis. Until then consider perhaps something like so
sealed trait OverallResult[+T]
case class OverallError(accumulatedMessage: String, finalErrorCode: Int) extends OverallResult[Nothing]
case class OverallSuccess[T](xs: IndexedSeq[T]) extends OverallResult[T]
object OverallResult {
/**
* Aggregating over a chain of Failures, it will only keep the exception type of the last Failure.
* This is just a heuristic to decide on the error code. Depending on the exception type, we use
* a different error code. So NoSuchElementException is 404 and IllegalArgumentException is 400.
*/
def apply[T](xs: IndexedSeq[Try[T]]): OverallResult[T] = {
val failures = xs.collect { case Failure(ex) => ex }
if (failures.nonEmpty) {
val accMessage = failures.map(_.getMessage).mkString("[", ",", "]")
OverallError(accMessage, errorCode(failures.last))
}
else OverallSuccess(xs.map(_.get))
}
private def errorCode(ex: Throwable): Int = ex match {
case _: NoSuchElementException => 404
case _: IllegalArgumentException => 400
case e => throw new RuntimeException("Unexpected exception. Fix ASAP!", e)
}
}
OverallResult(Vector(Try(throw new NoSuchElementException("boom")), Try(throw new IllegalArgumentException("crash"))))
OverallResult(Vector(Try(42), Try(11)))
which outputs
res0: OverallResult[Nothing] = OverallError([boom,crash],400)
res1: OverallResult[Int] = OverallSuccess(Vector(42, 11))
Note explicit documentation of the heuristic mentioned in the comments:
/**
* Aggregating over a chain of Failures, it will only keep the exception type of the last Failure.
* This is just a heuristic to decide on the error code. Depending on the exception type, we use
* a different error code. So NoSuchElementException is 404 and IllegalArgumentException is 400.
*/
Error accumulation is simulated with
failures.map(_.getMessage).mkString("[", ",", "]")
and overall status code decided with
errorCode(failures.last)
Now clients of extractTry
need to be refactored to pattern match on OverallResult
ADT, and finalErrorCode
instead of exceptions, but lower level codebase should remain unaffected.
Upvotes: 1