1. Introduction

Exception handling is a fundamental aspect of any software development. It helps to manage unexpected situations during the execution of the program effectively. Moreover, depending on the situation, we can gracefully recover from them without causing catastrophic failures.

Scala provides different ways to handle these situations efficiently. In this tutorial, we’ll discuss the difference between the types Exception and NonFatal in Scala. In addition, we’ll also go over the difference between Error and Exception.

2. Exception Hierarchy

In Scala, Throwable is the parent class for all exception-related classes. Let’s see a simplified exception hierarchy:

Exception Hierarchy in Scala

Error represents fatal issues that aren’t generally recoverable. Errors are caused by external reasons outside the running program’s control. Some of these errors are OutOfMemoryError, ThreadDeath, and so on.

On the other hand, Exception represents the conditions that can be handled or recovered from. For example, RuntimeException, NullPointerException, and IllegalStateException are some of the most common exception types.

ControlThrowable is a special type of Throwable that’s used internally by the Scala runtime to control the flow of the execution. We shouldn’t use this exception explicitly in the user codebase, as it can confuse the runtime.

3. Handling Exceptions

Scala provides two different ways to handle exceptions:

  • Using a try…catch block similar to Java
  • Using a Try monad

3.1. Using try…catch Block

We can use pattern matching in the catch block to handle different types of exceptions:

def failingFn(num: Int): String = {
  num match {
    case 0 => throw new RuntimeException("Number 0 is not allowed")
    case n if n < 0 =>
      throw new IllegalStateException("Number can't be negative")
    case _ => "Success"
  }
}
val handledResult =
  try {
    failingFn(-3)
  } catch {
    case is: IllegalStateException => "IllegalStateException"
    case rt: RuntimeException      => "RuntimeException"
    case _:                        => "Unknown"
  }
handledResult shouldBe "IllegalStateException"

In the above example, we used pattern matching to catch different types of exceptions and recover with a string value.

3.2. Using Try

Additionally, we can use the Try monad to handle exceptions in a more idiomatic way. We can rewrite the above code sample using the Try data structure:

def failingFn(num: Int): String = {
  num match {
    case 0 => throw new RuntimeException("Number 0 is not allowed")
    case n if n < 0 =>
      throw new IllegalStateException("Number can't be negative")
    case _ => "Success"
  }
}
val triedResult: Try[String] = Try(failingFn(-3))
val handledResult = triedResult match {
  case Success(value) => value
  case Failure(exception) =>
    exception match {
      case is: IllegalStateException => "IllegalStateException"
      case rt: RuntimeException      => "RuntimeException"
      case _                         => "Unknown"
    }
}
handledResult shouldBe "IllegalStateException"

Here, we can pattern-match the exception in the same way as we did in the catch block.

4. Difference Between Try and try…catch

Even though both Try and try…catch blocks handle the exceptions almost the same way, there’s a significant difference between them. As we saw in the exception hierarchy, Throwable is the parent for all, both fatal(errors) and non-fatal(exceptions).

The try…catch block handles all Throwables, including the errors in its catch block. As a result, the program tries to recover from a fatal and non-recoverable condition. On the other hand, the Try block only handles recoverable exceptions. Therefore, it excludes the Error and ControlThrowable types from handling.

We can verify this behavior by throwing an Error instead of an Exception in the sample code:

"catch all exceptions and errors using Throwable case block" in {
  def explodingFn: String =
    throw new LinkageError("Could not link the native library")
  def exceptionHandlingFn = {
    try {
      explodingFn
    } catch {
      case ex: Throwable =>
        s"Caught exception(${ex.getClass.getSimpleName}) from exception block"
    }
  }
  exceptionHandlingFn shouldBe "Caught exception(LinkageError) from exception block"
}

When we execute the above test, it passes successfully since the catch block can handle the LinkageError.

Now, let’s rewrite the same test using the Try block:

"Not be able to catch errors using Try block" in {
  def explodingFn: String =
    throw new LinkageError("Could not link the native library")
  def fnWithTry = {
    Try {
      explodingFn
    }
  }
  def tryToHandle = fnWithTry match {
    case Success(v)  => "success"
    case Failure(ex) => "Handled exception: " + ex.getClass.getSimpleName
  }
  // since it could not be handled, it should throw this linkage error.
  val intercepted = intercept[LinkageError](tryToHandle)
  // notice that it is not returning the message "Handled exception: LinkageError"
  intercepted.getMessage shouldBe "Could not link the native library"
}

Since the Try block cannot handle Error, it’s intercepted in the test using the intercept block. We should note that the error message comes from the original error, not the Failure block.

5. Handling NonFatal Exceptions

Now that we discussed the difference between errors and exceptions, let’s look at NonFatal. NonFatal isn’t a subtype of Throwable. Instead, it’s an Extractor object that handles only non-fatal conditions. Here’s its implementation:

object NonFatal {
  def apply(t: Throwable): Boolean = t match {
    case _: VirtualMachineError | _: ThreadDeath | _: InterruptedException | _: LinkageError | _: ControlThrowable => false
    case _ => true
  }
  def unapply(t: Throwable): Option[Throwable] = if (apply(t)) Some(t) else None
}

NonFatal extraction block handles all exceptions except VirtualMachineError, ThreadDeath, InterruptedException, LinkageError, ControlThrowable, and their subtypes.

The Try block uses this NonFatal extractor object to handle the exceptions. Consequently, it ignores these fatal errors as its responsibility:

"ignore fatal error from Try responsibility" in {
  def handleWithTry = Try {
    throw new VirtualMachineError("You are not strong enough to handle me!") {}
  }
  val errorMsg = intercept[VirtualMachineError](handleWithTry).getMessage
  errorMsg shouldBe "You are not strong enough to handle me!"
}

As we can see, the VirtualMachineError couldn’t be handled in the Try block, and it propagated further.

Using NonFatal to handle exceptions is always better as a practice. Here’s a simple way to use NonFatal in the code:

"not able to handle errors using NonFatal block" in {
  def result = try {
    throw new OutOfMemoryError("No Memory available")
  } catch {
    case NonFatal(_) => "Handled NonFatal Exceptions"
  }
  val errorMsg = intercept[OutOfMemoryError](result).getMessage
  errorMsg shouldBe "No Memory available"
}

The code above doesn’t catch the exception OutOfMemoryError as it’s a fatal error, and hence it’s propagated to the caller as it is.

6. Conclusion

In this article, we looked at the difference between fatal and non-fatal exceptions. We also discussed the differences between Try and try…catch block in Scala.

Ultimately, we learned about the NonFatal extractor object and its importance. We can conclude that it’s generally better to use NonFatal instead of Throwable while handling exceptions to avoid unintended situations. If we use Throwable in the catch block, even the errors such as LinkageError, OutOfMemoryErrors, and so on are caught and silenced, even though the program can’t handle these situations.

As always, the sample code used in this tutorial is available over on GitHub.