1. Introduction

In this tutorial, we’re going to talk about exception handling in Scala. We’ll explore different ways of handling them, using different constructs provided by the language.

2. What’s an Exception?

An exception is an event that changes the normal flow of a program.

Exception handling is the mechanism to respond to the occurrence of an exception.

Exceptions can be checked or unchecked. Scala only allows unchecked exceptions, though. This means that, at compile-time, we won’t be able to know if a method is throwing an exception we are not handling.

3. A Basic Calculator

Let’s write a simple calculator to show different ways of handling exceptions in Scala. It’ll only add positive integer numbers:

object CalculatorExceptions {
  class IntOverflowException extends RuntimeException
  class NegativeNumberException extends RuntimeException
}

object Calculator {
  import CalculatorExceptions._

  def sum(a: Int, b: Int): Int = {
    if (a < 0 || b < 0) throw new NegativeNumberException
    val result = a + b
    if (result < 0) throw new IntOverflowException
    result
  }
}

Our method could throw a NegativeNumberException if one of the addends is negative, or IntOverflowException if the sum overflows the Int range.

When using our calculator, we’ll have to consider how to handle those exceptions.

In the following paragraphs, we’ll see different ways of doing it in Scala.

4. try/catch/finally

A basic way we can handle exceptions in Scala is the try/catch/finally construct, really similar to the Java one.

In the following example, to make testing easier, we’ll return a different negative error code for each exception caught:

def tryCatch(a: Int, b: Int): Int = {
  try {
    return Calculator.sum(a,b)
  } catch {
    case e: IntOverflowException => -1
    case e: NegativeNumberException => -2
  } finally {
    // This block will always be invoked
    println("Calculation done!")
  }
}

Some points to notice about the above example:

  • the “risky” code goes in the try block
  • the code in the finally block will always be executed, no matter what happens before – this block is handy, for example, whenever we want to make sure we close a resource like a database connection
  • case statements can be used in the catch section to match different exception types

With this syntax, we’re forced to decide what to do with the exceptions as soon as they are thrown. In our example, we’re returning different error codes depending on the exception we caught.

5. Try/Success/Failure

Try[T] is an Algebraic Data Type, whose instances are Success[T] and Failure[T].

Let’s rewrite our tryCatch method using it:

def trySuccessFailure(a: Int, b: Int): Try[Int] = Try {
  Calculator.sum(a,b)
}

Now, we can use some tests to show how the result of trySuccessFailure is used in a functional style:

"trySuccessFailure" should "handle NegativeNumberException" in {
  import CalculatorExceptions._
  val result = trySuccessFailure(-1,-2)
  result match {
    case Failure(e) => assert(e.isInstanceOf[NegativeNumberException])
    case Success(_) => fail("Should fail!")
  }
}

it should "handle IntOverflowException" in {
  import CalculatorExceptions._
  val result = trySuccessFailure(Int.MaxValue,1)
  result match {
    case Failure(e) => assert(e.isInstanceOf[IntOverflowException])
    case Success(_) => fail("Should fail!")
  }
}

it should "return the correct sum" in {
  import CalculatorExceptions._
  val result = trySuccessFailure(3,2)
  result match {
    case Failure(e) => fail("Should succed!")
    case Success(result) => assert(result == 5)
  }
}

Our method will return either a Succes or a Failure class. Using a pattern match, we can easily handle the result of our function.

6. Catch Objects

Another way of catching exceptions comes from the scala.util.control.Exception object. Let’s use a catch object to handle our Calculator.sum:

def catchObjects(a: Int, b: Int): Try[Int] = allCatch.withTry {
  Calculator.sum(a,b)
}

The allCatch.withTry object allows us to catch all the exceptions and handle them with a Try. The above code will behave exactly as the trySuccessFailure we previously implemented.

scala.util.control.Exception also provides out-of-the-box opt and either to wrap the exception in, respectively, an Option or an Either.

An interesting feature of catch objects is the possibility of defining custom matchers. To see how simple they are to define, we can write one that only handles NegativeNumberException:

val myCustomCatcher = catching(classOf[NegativeNumberException])

def customCatchObjects(a: Int, b: Int): Try[Int] = myCustomCatcher.withTry{
  Calculator.sum(a,b)
}

Moreover, we can use the scala.util.control.Exception.ignoring() catch object to catch and ignore the specified exceptions:

def ignoringAndSum(a: Int, b: Int) =
  ignoring(classOf[NegativeNumberException], classOf[IntOverflowException]) {
    println(s"Sum of $a and $b is equal to ${Calculator.sum(a, b)}")
  }

This will execute the pass block of code while ignoring the mentioned exceptions.

Let’s use some tests to show the behavior of our new matcher:

"customCatchObjects" should "handle NegativeNumberException" in {
  import CalculatorExceptions._
  val result = customCatchObjects(-1,-2)
  result match {
    case Failure(e) => assert(e.isInstanceOf[NegativeNumberException])
    case Success(_) => fail("Should fail!")
  }
}

it should "handle IntOverflowException" in {
  import CalculatorExceptions._
  assertThrows[IntOverflowException] {
    customCatchObjects(Int.MaxValue,1)
  }
}

it should "return the correct sum" in {
  import CalculatorExceptions._
  val result = customCatchObjects(3,2)
  result match {
    case Failure(e) => fail("Should succed!")
    case Success(result) => assert(result == 5)
  }
}
it should "ignore specified exceptions" in {
  Examples.ignoringAndSum(-1, -2)
}

As we can see, in case an IntOverflowException exception is thrown, it will not be handled.

Catch objects can be handy to centralize the exception handling logic and avoid repetitious code.

However, they are less known compared to Try/Success/Failure.

7. Functional Composability

So far, we’ve been able to defer the handling of the exceptions to a later stage. This is essential when we are aiming for functional composability.

We can combine our methods to better demonstrate how the handling of the exceptions can be done at the end of the chain:

"customCatchObjects composed with trySuccessFailure" should "return the correct sum" in {
  import CalculatorExceptions._
  val result = customCatchObjects(3, 2) flatMap (trySuccessFailure(_, 3))
  result match {
    case Failure(e)      => fail("Should succed!")
    case Success(result) => assert(result == 8)
  }
}

it should "print an error" in {
  import CalculatorExceptions._
  val result = customCatchObjects(-1, -2) flatMap (trySuccessFailure(_, 3))
  result match {
    case Failure(e)      => println("Found error!")
    case Success(result) => fail("Should fail!")
  }
}

In the code above, we decide what to do with our exception at the very end. When writing our trySuccessFailure and customCatchObjects, we didn’t have to think about it.

Both Try/Success/Failure and catch objects help to write functional composable code. They allow deferring the evaluation of the exceptions, instead of dealing with them straight away as forced by the classical try/catch.

8. Conclusion

In this article, we talked about Scala’s support for exception handling.

When choosing between catch objects and Try/Success/Failure, the trade-off is between code accessibility and code reusability. In Scala, they are definitively preferable to try/catch/finally since they provide an easier way to achieve functional composability.

As always, the code is available over on GitHub.

guest
0 Comments
Inline Feedbacks
View all comments