30

Scala's Try is very useful.

I'd like to use that pattern, but log all exceptions.

How can I do this?

Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
SRobertJames
  • 8,210
  • 14
  • 60
  • 107
  • I will come back and write an official answer later. However, here's a blog post I just finished addressing just this issue (and others related): http://fromjavatoscala.blogspot.com/2016/09/rethinking-scalautiltry.html – chaotic3quilibrium Oct 01 '16 at 18:25

4 Answers4

34

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

Guillermo Gutiérrez
  • 17,273
  • 17
  • 89
  • 116
sjrd
  • 21,805
  • 2
  • 61
  • 91
  • 1
    Will try out. BTW, to match Try source code https://github.com/scala/scala/blob/v2.11.1/src/library/scala/util/Try.scala#L192 , shouldn't it only catch NonFatal exceptions? – SRobertJames Jun 24 '14 at 19:31
  • @SRobertJames - No, because it already has only caught NonFatal exceptions, so there are only those to recover from. Unless you mean: can I log all exceptions from Try _including those it can't catch_? Then the answer is no; you have to insert your own custom logging code. – Rex Kerr Jun 24 '14 at 19:50
  • This solution will not emit logging if the `Throwable` is !NonFatal. – chaotic3quilibrium Oct 01 '16 at 22:11
7

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

Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
5

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.

goral
  • 1,275
  • 11
  • 17
1

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.

chaotic3quilibrium
  • 5,661
  • 8
  • 53
  • 86