3

While dealing with error-handling in Scala, I came to the point where I asked myself, whether Trys in a for-comprehension make sense.

Please regard the unit test given below. This test shows two approaches:

  1. The first approach (with call) embeds the methods fooA and fooB, which return regular Strings, into a Try construct.
  2. The second approach (tryCall) uses a for-comprehension that uses methods tryFooA and tryFooB, which return Try[String] each.

For what reason should one prefer the for-comprehension variant with tryCall over the call-variant?

test("Stackoverflow post: Try and for-comprehensions.") {

  val iae = new IllegalArgumentException("IAE")
  val rt = new RuntimeException("RT")

  import scala.util.{Try, Success, Failure}

  def fooA(x1: Int) : String = {
    println("fooA")
    if (x1 == 1) "x1 is 1" else throw iae
  }

  def fooB(x2: Int) : String = {
    println("fooB")
    if (x2 == 1) "x2 is 1" else throw rt
  }

  def tryFooA(x1: Int) : Try[String] = {
    Try {
      println("tryFooA")
      if (x1 == 1) "x1 is 1" else throw iae
    }
  }

  def tryFooB(x2: Int) : Try[String] = {
    Try {
      println("tryFooB")
      if (x2 == 1) "x2 is 1" else throw rt
    }
  }

  def call( x1: Int, x2: Int ) : Try[String] = {
    val res: Try[String] = Try{
      val a = fooA(x1)
      val b = fooB(x2)
      a + " " + b
    }
    res
  }

  def tryCall( x1: Int, x2: Int ): Try[String] = {
    for {
      a <- tryFooA(x1)
      b <- tryFooB(x2)
    } yield (a + " " + b)
  }

  assert( call(0,0) === tryCall(0,0))
  assert( call(0,1) === tryCall(0,1))
  assert( call(1,0) === tryCall(1,0))
  assert( call(1,1) === tryCall(1,1))
}
Martin Senne
  • 5,939
  • 6
  • 30
  • 47

1 Answers1

3

The purpose of Try is to let the compiler help you (and anyone else who uses your code) handle and reason about errors more responsibly.

In your examples, the behavior of call and tryCall are more or less identical, and if the fooA, etc. methods were not part of any public API, there'd be little reason to prefer one over the other.

The advantage of Try is that it lets you compose operations that can fail in a clear, concise way. The signatures of tryFooA and tryFooB are upfront about the fact that they may result in (recoverable) failure, and the compiler makes sure that anyone who calls those methods must deal with that possibility. The signatures of fooA and fooB aren't self-documenting in this way, and the compiler can't provide any assurances that they'll be called responsibly.

These are good reasons to prefer the tryFoo signatures. The downside is that callers have to deal with the extra overhead of using map, flatMap, etc., instead of working with the results (if they exist) directly. The for-comprehension syntax is an attempt to minimize this overhead—you get the safety provided by Try with only a little extra syntactic overhead.

Travis Brown
  • 138,631
  • 12
  • 375
  • 680