Learn about the various approaches to exception handling in Scala, and discover a peculiar situation that may arise while using scala.util.Try. This blog post delves into the fact that scala.util.Try is designed to handle only NonFatal exceptions, and explores the benefits of placing potentially problematic code within a try..catch block to obtain additional information. Additionally, this article offers valuable resources for further reading on Scala Exceptions and alternative methods for managing them.
1. Introduction
Exception handling is one of the most important and basic features of any programming language. There are many articles and blogs explaining the way we can handle exceptions in Scala.
I am not going to repeat the same things in this blog, and I will be leaving links to some of the resources available on the internet at the end of this blog. However, I would like to explain a particular scenario that might happen with exception handling in Scala, especially for newbies.
2. Approaches for Exception Handling
As you probably know, there are multiple ways in Scala to handle the exceptions, such as scala.util.Try
, java style try..catch
block, more functional ways like Either
and so on.
Most people will be using scala.util.Try
to handle exception scenarios. As a general practice, we would be wrapping the code block that might cause exceptions within the Try block, thus transforming the exception as data.
3. Strange Scenarios
As we discussed before if we are using scala.util.Try
, we wrap the suspicious code within Try block and expect that if any exceptions are thrown it will make the result to be a Failure. Let's look at a simple example:
import scala.util._
object Test extends App {
def someJVMMethod(): String = {
throw new Exception("This is an exception")
}
val res: Try[String] = Try {
someJVMMethod()
}
res match {
case Success(s) => println("Successfully received: "+s)
case Failure(ex) => println("Uh oh.. "+ex.getMessage)
}
}
When we execute this app, it will print the message as below, since the Try contains the Failure details.
Uh oh.. This is an exception to the console
However, assume that we are invoking a method from a third-party library within someJVMMethod()
, for example, starting an embedded MongoDB server or an h2 database or something else. Since we have multiple libraries in the dependencies and there could be multiple versions of the same libraries within our application's classpath due to transitive dependencies. Due to this, there is a chance that this might lead to strange errors like binary compatibility issues between libraries.
If that happens within the method someJVMMethod()
, let's see how the app behaves. For example, we can explicitly throw an IncompatibleClassChangeError
to simulate the scenario. Let's re-write the method someJVMMethod()
as:
def someJVMMethod(): String = {
throw new IncompatibleClassChangeError("Binary compatibilty issue")
}
If we execute the same code with this method instead, it will neither print the statements from the Success nor the Failure branches and instead just crashes without logging any details.
As the next step, let's add the try..catch
block to the mix now and rewrite the way we invoke the method:
val res: Try[String] = Try {
try {
someJVMMethod()
} catch {
case ex =>
println("Message from catch.. "+ex)
throw new RuntimeException(ex.getMessage())
}
}
If we execute this now, we can see the output in the console and no crashing of the app:
Message from catch.. java.lang.IncompatibleClassChangeError: Binary compatibilty issue Uh oh.. Binary compatibilty issue
4. Exploring the Reason for the Crash
Now, let's explore the reason for this behaviour. If we look at the source code of the exception class IncompatibleClassChangeError
, we can see that it is extending LinkageError
which extends Error
, which again extends Throwable
. As a general practice, we are supposed to handle only Exception
and not Error
, as errors are something which can't(should't) be recovered at runtime.
Now, if we look at the source code of Try, we can see that it actually uses try..catch
under the hood, but handles only exceptions of type NonFatal. NonFatal
is defined in Scala as all Throwable
except VirtualMachineError
, ThreadDeath
, InterruptedException
, LinkageError
and ControlThrowable
.
That means, scala.util.Try will handle only NonFatal exceptions. Since we got IncompatibleClassChangeError
which is LinkageError
, it was not handled by the scala.util.Try
block and hence caused this strange behaviour. However, since try..catch
block can catch any Throwable, we were able to handle it.
5. Conclusion
In this blog, we looked at some cases where scala.util.Try behaves differently from try..catch block. Even though we generally should not handle fatal errors, it is necessary to exactly identify the root cause for these errors when they occur. So in such cases, we can wrap the suspecting code within a plain old try..catch to get more details.
I hope this information comes in handy for you.
Here are some of the resources available to read more about Scala Exceptions and different ways to handle them.
baeldung.com/scala/exception-handling
baeldung.com/scala/error-handling
docs.scala-lang.org/scala3/book/fp-function..
blog.rockthejvm.com/idiomatic-error-handlin..