Skip to content

Latest commit

 

History

History
156 lines (125 loc) · 7.1 KB

06 try.md

File metadata and controls

156 lines (125 loc) · 7.1 KB

Try

In Java you have to state explicitly that a certain method can throw an exception. That is, if the exception is not a RuntimeException. When this exception throwing method is used in a second method, it must handle the exception by either propagating it up the stack (rethrowing) or catching it and providing an alternative value.

// this is Java code!
public static void foo throws Exception {
    throw new Exception();
}

public static void bar1() throws Exception {
    foo();
}

public static void bar2() {
    try {
        foo();
    }
    catch (Exception e) {
        // do something
    }
}

In Scala you can throw exceptions as well, but there is no specification such as Java's throws Exception in the function declaration. This makes the type of your functions less safe and can cause unexpected bugs and failures. The compiler cannot check whether an error can occur somewhere and is therefore not able to warn you about it.

def foo: Unit = {
    throw new Exception()
}

If this method is not documented properly and/or you don't have access to the source code, you will only find out at runtime that this function throws an Exception. This is not visible in the return type!

Scala instead provides a technique that is quite similar to its Option type. Whereas this type denoted the possibility of returning a null, a Try type expresses the possibility of returning an Exception instead of an actual value.

Try has two subclasses: Success (which holds an actual value) and Failure (which holds an exception). The approach here is to not throw the Exception but return it, wrapped in a Failure class.

def funckySum(a: Int, b: Int): Try[Int] = {
  val sum = a + b
  if (sum == 5) Success(sum)
  else Failure(IllegalArgumentException("sum isn't 5, sorry, can't return it :("))
}

funckySum(3, 2)         // returns Success(5)
funckySum(4, 1)         // returns Success(5)
funckySum(4, 3)         // returns Failure(IllegalArgumentException(...))

If some function (for instance in third party libraries) does throw exceptions, we can easily catch them by using a Try.apply (remember, you don't need to write the apply!). This function automatically catches the exception and wraps them into a Failure. Note that this is a fail-fast approach, just like the try-catch blocks!

def funckySum(a: Int, b: Int): Try[Int] = Try {
  val sum = a + b
  if (sum == 5) sum
  else throw IllegalArgumentException("sum isn't 5, sorry, can't return it :(")
}

funckySum(3, 2)         // returns Success(5)
funckySum(4, 1)         // returns Success(5)
funckySum(4, 3)         // returns Failure(IllegalArgumentException(...))

It should not come as a surprise that Try has also a number of operators defined on it, such that you can transform it in the same way as Option and List. Again we have map, flatMap, filter and foreach defined on it, which only execute the inner functions in the case a Try is a Success. In the case of a Failure, these lambdas are not executed and the original Exception is propagated.

An interesting thing happens when using filter. If the predicate does not hold for the value inside Try (in case it is a Success!), the Try will be converted into a Failure containing a NoSuchElementException. This may be useful on one hand, but can also be unintended, as you probably might want to have control over which type of exception to be 'returned' as well as the message it contains. For this reason you might as well do the filtering and predicate checking yourself and come up with a meaningful error message, as shown in the code below:

def evenNumber(n: Int): Try[Int] = {
	Try(n).filter(x => x % 2 == 0)
}

def evenNumberManually(n: Int): Try[Int] = {
	if (n % 2 == 0) Success(n)
	else Failure(new IllegalArgumentException(s"$n is not even"))
}

evenNumber(4)               // returns Success(4)
evenNumber(5)               // returns Failure(java.util.NoSuchElementException: Predicate does not hold for 5)

evenNumberManually(4)       // returns Success(4)
evenNumberManually(5)       // returns Failure(java.lang.IllegalArgumentException: 5 is not even)

Besides the operators mentioned above, Try defines a set of operators that can do error handling, which is equivalent to the catch block in a try-catch expression. There is a same kind of getOrElse operator as is defined on Option, which returns the value inside a Try if it is a Success and returns a default value if it is a Failure. However, this operator does not discriminate between the various types of exceptions that can be contained in a Failure.

evenNumber(4).getOrElse(2)      // returns 4
evenNumber(5).getOrElse(2)      // returns 2

To do so, we can use operators like recover and recoverWith. These both take a so-called 'partial function', meaning that you do not have to define the function for every type of input. Instead you pattern match on the input and write code for the cases you want to recover from. Cases that are not covered by the pattern match will just be propagated. This means that the return type of a recover or recoverWith operator always has to return a Try as well. It only transforms certain failure cases into successes.

When using recover, you match a certain kind of exception and return a value that you want to have instead. This value will then be wrapped in a Success. In the code below you see this pattern matching and the use of recover in action. Since evenNumber returns a NoSuchElementException when the input is odd, it will transform to a Success(-1). On the other hand, evenNumberManually returns a IllegalArgumentException, which is not covered by the pattern match. Therefore the original result is just propagated. In a sense, recover can be thought of as the map operator's equivalent for Failure cases.

evenNumber(4).recover {                     // returns Success(4)
	case e: NoSuchElementException => -1
}

evenNumber(5).recover {                     // returns Success(-1)
	case e: NoSuchElementException => -1
}

evenNumberManually(4).recover {             // returns Success(4)
	case e: NoSuchElementException => -1
}

evenNumberManually(5).recover {             // returns Failure(java.lang.IllegalArgumentException: 5 is not even)
	case e: NoSuchElementException => -1
}

On the other hand, we can use recoverWith. This operator looks like recover, but its inner function (which is again a partial function) doesn't return a value, but returns another Try instead. For example, in the code below, the NoSuchElementException returned by the filter in evenNumber is transformed into an exception with a more meaningful message.

evenNumber(4).recoverWith {         // returns Success(4)
	case e: NoSuchElementException => Failure(new IllegalArgumentException(s"the input wasn't an even number"))
}

evenNumber(5).recoverWith {         // returns Failure(java.lang.IllegalArgumentException: the input wasn't an even number)
	case e: NoSuchElementException => Failure(new IllegalArgumentException(s"the input wasn't an even number"))
}