At first glance, the try-catch block seems to be the preferred approach to deal with exceptions for the people coming to Scala from Java. However, in reality, this approach is not a single one available in Scala.
Data Engineering Design Patterns

Looking for a book that defines and solves most common data engineering problems? I wrote
one on that topic! You can read it online
on the O'Reilly platform,
or get a print copy on Amazon.
I also help solve your data engineering problems 👉 contact@waitingforcode.com 📩
This post presents the aspect of exceptions in Scala. The first part introduces the idea of "something that can go wrong". The next section shows how to deal with exceptions in a Java-oriented way through try-catch blocks in Scala. The third part describes a more functional way to handle bad things - Try type. The last section talks about Exception utility methods.
Scala and exceptions
If you've read or write some Scala code you've certainly notice the lack of throws keyword in method signatures. Do not let you mislead by @throws annotation. It's purely informational and doesn't enforce the caller to handle the declared exception:
describe("@throws annotation") { it("should not enforce caller to handle the exception") { @throws[IllegalArgumentException]("if the word is empty") def getLetters(word: String) = word.split("") // As you can see, we can call the method without try-catch block - even though // the IllegalArgumentException may be thrown val letters = getLetters("abc") letters should have size 3 letters should contain allOf("a", "b", "c") } }
Does Scala ban the exceptions ? Not really but it discourages their use because of their impact on functional programming properties as pure functions and referential transparency. Exceptions add unpredictability to the functions and because of that the functions lose their purity.
It's the reason why Scala extends the way of thinking about exceptions to not only constructs changing program flow. In Scala we can consider exceptions, or generally things going wrong, as data structures carrying an information about unexpected behavior. The latter aspect will be detailed in one of next posts.
The exceptions in Scala are then discouraged to keep the code purity but they're not banned. As always their use depends on the context, project conventions, programming habits and ability to change them.
Try-catch block
The most basic way to handle exceptions in Scala is try-catch block known from many other programming languages:
describe("try-catch block") { it("should catch the NPE") { def getValue(optionToGet: Option[String]): String = { try { optionToGet.get } catch { case nsee: NoSuchElementException => s"NoSuchElementException found: ${nsee.getMessage}" case NonFatal(e) => s"NonFatal exception found: ${e.getMessage}" } } val exceptionMessageForNone = getValue(None) val exceptionMessageForNull = getValue(null) exceptionMessageForNone shouldEqual "NoSuchElementException found: None.get" exceptionMessageForNull shouldEqual "NonFatal exception found: null" } }
Noticed something strange ? Pattern matching to handle exception types is pretty intuitive but the type of one of the exceptions, scala.util.control.NonFatal, is much less. The class is in fact an extractor (you can learn more about in the post Scala extractors) for all non fatal types. Concretely it means that any of exceptions being an instance of VirtualMachineError, ThreadDeath, LinkageError, InterruptedException and ControlThrowable will be caught in the block. Although we can still catch them with the help of guards:
it("should catch Fatal error with guards") { def throwFatalError(): Unit = { throw new OutOfMemoryError() } val error = try { throwFatalError() } catch { case NonFatal(_) => "NonFatal" case error if !NonFatal(error) => "Fatal" } error shouldEqual "Fatal" }
Why distinguish both fatal and not fatal exceptions? It helps to adapt the program's behavior to the bad things that would normally happen, as for instance an index out of bounds or NullPointerException. JVM errors considered as fatal are the things that wouldn't normally happen in the execution flow unless some serious errors as a memory leak. Handling such kind of errors in the code would even lead to making JVM unavailable.
Try type
More Scala-like manner to deal with exceptions is scala.util.Try type. It represents the computation that may succeed or fail. For the former case Try returns Success and for the latter Failure:
describe("Try type") { def failFromInput(word: String): String = { if (word == "a") { throw new IllegalArgumentException("a is not accepted") } "success !" } it("should catch a failed execution") { val executionResult = Try(failFromInput(("a"))) executionResult.isSuccess shouldBe false executionResult.isFailure shouldBe true executionResult.getOrElse("error") shouldEqual "error" val exception = executionResult.failed.get exception.getMessage shouldEqual "a is not accepted" } it("should catch a successful execution") { val executionResult = Try(failFromInput(("b"))) executionResult.isSuccess shouldBe true executionResult.isFailure shouldBe false executionResult.getOrElse("error") shouldEqual "success !" } }
Try type appears here as a shorter alternative for try-catch block able to retrieve a computed value or an occurred exception. Also unlike the block, Try is a monad composable other operations as map or filter:
it("should be chained in filter-map operations") { val operations: Seq[Try[String]] = Seq(Success("a"), Failure[String](new NullPointerException("")), Success("b")) val succeededOperations = operations.filter(tryResult => tryResult.isSuccess) .map(tryResult => tryResult.get) succeededOperations should have size 2 succeededOperations should contain allOf("a", "b") }
Try is also able to call recovery partial function, invoked only when the tried operation fails:
it("should recover from exception") { val letter = Try(failFromInput(("a"))).recover { case NonFatal(e) => "A" } letter shouldEqual Success("A") }
Under-the-hood Try simply wraps try-catch block and returns the type appropriated to the result:
def apply[T](r: => T): Try[T] = try Success(r) catch { case NonFatal(e) => Failure(e) }
An open debate concerns Try as returned type. From one side it tells the caller that the called operation may go wrong in some cases. Thanks to that the caller can prepare itself to handle the error. In the other side returned Try is the synonymous of Java's checked exceptions (throw Exception in method's signature). Wrapping the code at caller side seems to be more practical unless it doesn't happen at every line which can reduce the readability.
Exception methods
The last exception handling method described in this post is scala.util.control.Exception class. It represents the components we can use in exceptions handling, as for instance:
- catching - declares the exceptions to catch and adapts the behavior (rethrow exception, execute alternative code):
describe("control Exception class") { it("should wrap a risky method and return alternative value for handled exception") { val catchingBlock = catching(classOf[IllegalStateException], classOf[IllegalArgumentException]) val illegalStateError = catchingBlock.withApply { case NonFatal(_) => { "XYZ" } } val resultForErrorCase = illegalStateError.apply(throw new IllegalStateException("")) resultForErrorCase shouldEqual "XYZ" } it("should wrap a risky method and fail for unhandled exception") { val catchingBlock = catching(classOf[IllegalStateException], classOf[IllegalArgumentException]) val illegalStateError = catchingBlock.withApply { case NonFatal(_) => { "XYZ" } } val caughtError = intercept[NullPointerException] { illegalStateError.apply(throw new NullPointerException("")) } caughtError shouldBe a [NullPointerException] } it("should build try-catch-finally block") { var finallyBlockCalled = false val tryCatchFinallyBlock = catching[String](classOf[NullPointerException]) .andFinally({ finallyBlockCalled = true }) val value = tryCatchFinallyBlock.withApply(_ => "NPE").apply(throw new NullPointerException("")) finallyBlockCalled shouldBe true value shouldEqual "NPE" } }
- ignoring - ignores listed exceptions:
it("should catch and ignore supplied exceptions") { val catchingBlock = ignoring(classOf[NullPointerException]) catchingBlock.apply(throw new NullPointerException("")) // As you can see, the block ignored NPE but it'll throw IllegalStateException val interceptedException = intercept[IllegalStateException] { catchingBlock.apply(throw new IllegalStateException("")) } interceptedException shouldBe a [IllegalStateException] }
- failing - this method maps listed exceptions to None type:
it("should map listed exceptions to None") { val failingBlock = failing(classOf[NullPointerException]) val valueFromNpe = failingBlock.apply(throw new NullPointerException("")) valueFromNpe shouldBe empty // As you can see, the block ignored NPE but it'll throw IllegalStateException val interceptedException = intercept[IllegalStateException] { failingBlock.apply(throw new IllegalStateException("")) } interceptedException shouldBe a [IllegalStateException] }
- failAsValue - similar to the previous point except it lets to customize returned type in case of any of listed exceptions:
it("should map listed exceptions to a 'failure value'") { val failingValueBlock = failAsValue(classOf[NullPointerException], classOf[IllegalArgumentException])(".") val valueFromNpe = failingValueBlock.apply(throw new NullPointerException("")) valueFromNpe shouldEqual "." // As you can see, the block mapped NPE but it'll throw IllegalStateException val interceptedException = intercept[IllegalStateException] { failingValueBlock.apply(throw new IllegalStateException("")) } interceptedException shouldBe a [IllegalStateException] }
- handling - defines the function handling one of listed exceptions:
it("should handle listed exceptions") { val handlingBlock = handling[String](classOf[NullPointerException], classOf[IllegalStateException]) val errorsContainer = new mutable.ListBuffer[String]() def handleError(exception: Throwable): String = { exception match { case _: NullPointerException => "NPE" case _: IllegalStateException => "ISE" } } val blockWithHandler = handlingBlock.by(handleError) val errorCode = blockWithHandler.apply(throw new IllegalStateException("")) errorCode shouldEqual "ISE" // Unsurprisingly it doesn't work for not declared exceptions val interceptedException = intercept[IllegalArgumentException] { blockWithHandler.apply(throw new IllegalArgumentException("")) } interceptedException shouldBe a [IllegalArgumentException] }
- unwrapping - unwraps wrapped exceptions, that said, it returns them directly:
it("should unwrap listed exceptions") { val unwrappingLogic = unwrapping(classOf[NullPointerException]) val interceptedNpe = intercept[NullPointerException] { unwrappingLogic.apply(throw new NullPointerException("")) } interceptedNpe shouldBe a [NullPointerException] }
Even though functional purists ban the use of exceptions because of broken referential transparency, the exceptions exist. Either in 3rd party Java libraries or simply to fail fast the execution of the application when it can't work without data of failed function. Scala provides 3 different ways to deal with unrecoverable errors. The first one is classical try-catch block and it's helpful to delegate exception handling to the caller. The other one is a variation of the block, Try type. Through its Success or Failure representation, it tells whether the operation succeeded or not. The last option comes from scala.util.control.Exception methods and as we saw in the examples, it lets us define a reusable behavior for different strategies of exceptions handling.
Consulting

With nearly 16 years of experience, including 8 as data engineer, I offer expert consulting to design and optimize scalable data solutions.
As an O’Reilly author, Data+AI Summit speaker, and blogger, I bring cutting-edge insights to modernize infrastructure, build robust pipelines, and
drive data-driven decision-making. Let's transform your data challenges into opportunities—reach out to elevate your data engineering game today!
👉 contact@waitingforcode.com
đź”— past projects