Scala\'s Try
is very useful.
I\'d like to use that pattern, but log all exceptions.
How can I do this?
You used the term "exceptions" which is ambiguous. (java.lang.)Throwable is the root of anything that can be placed behind the throw
term. java.lang.Exception is one of the two descendants of Throwable (the other being java.lang.Error). Further making this ambiguous is java.lang.RuntimeException, a descendant of Exception
, which is probably where you mostly want to spend your logging time (unless you are doing lower level application framework or hardware driver implementations).
Assuming you are wanting to log literally ALL instances of Throwable, then you would need something like this (NOT RECOMMENDED):
def logAtThrowable(f: => A): Try[A] =
try
Try(f) match {
case failure @ Failure(throwable) =>
log(s"Failure: {throwable.getMessage}")
failure
case success @ _ =>
//uncomment out the next line if you want to also log Success-es
//log(s"Success: {throwable.getMessage}")
success
}
catch throwable: Throwable => {
//!NonFatal pathway
log(s"Failure: {throwable.getMessage}")
throw throwable
}
The external try/catch
is required to capture all the Throwable
instances which are filtered away by scala.util.control.NonFatal within the Try
's try
/catch
block.
That said...there is a Java/JVM rule: you should never define a catch clause at the resolution of Throwable (again, unless you are doing lower level application framework or hardware driver implementations).
Following the intention of this rule, you would need to narrow the Throwable
to you only emitted logging at the finer grained level, say something more refined, like java.lang.RuntimeException
. If so, the code would look like this (recommended):
def logAtRuntimeException(f: => A): Try[A] =
Try(f) match {
case failure @ Failure(throwable) =>
throwable match {
case runtimeException: RuntimeException =>
log(s"Failure: {runtimeException.getMessage}")
}
failure
case success @ _ =>
success
}
In both code snippets above, you will notice that I used match
as opposed to .recoverWith
. This is to facilitate easily adding a rethrow
that works. It turns out that all the methods on Try
are themselves also wrapped with try
/catch
blocks. This means that if you want to log the Throwable
and then rethrow
it, if you are using one of the Try
methods like recoverWith
, the rethrow
is immediately recaught and placed into a Failure
thereby completely undermining the value of the intentional rethrow
. By using match
, the rethrow
is guaranteed to succeed as it remains outside any of the Try
methods.
If you would like to see more of the rabbit holes around this particular area, I created a blog post of my own exploration.
You can tweak it even further using implicit class
def someMethod[A](f: => A): Try[A] = Try(f)
implicit class LogTry[A](res: Try[A]) {
def log() = res match {
case Success(s) => println("Success :) " + s); res
case Failure(f) => println("Failure :( " + f); res
}
}
Now you can call someMethod
and on its result call log
like this:
scala> someMethod(1/0).log
Failure :( java.lang.ArithmeticException: / by zero
and
scala> someMethod(1).log
Success :) 1
Of course println
method inside implicit class can be substituted with any logging you want.
Starting Scala 2.13
, the chaining operation tap can be used to apply a side effect (in this case some logging) on any value while returning the original value:
import util.chaining._
val x = Try("aa".toInt).tap(_.failed.foreach(println))
// java.lang.NumberFormatException: For input string: "aa"
// x: Try[Int] = Failure(java.lang.NumberFormatException: For input string: "aa")
Or an equivalent pattern matching version:
val x = Try("aa".toInt).tap { case Failure(e) => println(e) case _ => }
// java.lang.NumberFormatException: For input string: "aa"
// x: Try[Int] = Failure(java.lang.NumberFormatException: For input string: "aa")
The tap chaining operation applies a side effect (in this case println
or some logging) on a value (in this case a Try
) while returning the original unmodified value on which tap
is applied (the Try
):
def tap[U](f: (A) => U): A
Define the following helper:
import scala.util.{Try, Failure}
def LogTry[A](computation: => A): Try[A] = {
Try(computation) recoverWith {
case e: Throwable =>
log(e)
Failure(e)
}
}
Then you can use it as you would use Try
, but any exception will be logged through log(e)
.