Coming from the JavaScript ecosystem, I wanted to learn functional programming without drowning in unfamiliar syntax. Scala hit the sweet spot — familiar enough (JVM-based, C-style syntax) but genuinely functional. This is the first in a series exploring Scala’s most eye-opening features, starting with how it handles errors without throwing exceptions.
Imperative try/catch
Lets imagine we have a flakey API that we have to build against in Scala:
def flakeyApi(): String = {
val randomIntBounded = Random.nextInt(100) // 0 to 99
if(randomIntBounded < 50) throw new RuntimeException("A dummy failure")
return "It worked!"
}
An imperative try/catch as you expect looks like this in Scala
val traditionalTry: String =
try {
flakeyApi()
} catch {
case error: RuntimeException => "it did not work, but I recovered here!"
case _: Throwable => "Something really bad happened"
}
println(traditionalTry)
The value of traditionalTry
is assigned either the return value of flakeyApi
or one of the values in the catch block based on the type of exception that gets thrown.
Try Monad
Scala ships with a Try
Monad. A Try
Monad wraps a function and if the function succeeds, the return value will be Success
if the function doesn’t throw, otherwise Failure
Here’s a very simple example —
val functionalTry: Try[String] = Try(flakeyApi())
.recover {
case error: RuntimeException => "it did not work, but I recovered here!"
case _ => "Something really bad happened"
}
So why the need for the functional version at all? Well, throwing an exception is a considered a side effect since they can terminate a program all together, which in functional programming we don’t want because this means a function can produce a value outside of its return value and they also break referential transparency (A function that throws an exception can’t be replaced by its return value).
A nice feature of this style of handling failure is the composability you have:
val functionalTry: Try[String] = Try(flakeyApi())
.orElse(Try(lessFlakeyApi()))
.map(successValue => successValue.toUpperCase)
.flatMap(message => Try(aFullyWorkingApi(message)))
.recover {
case error: RuntimeException => "it did not work, but I recovered here!"
case _ => "Something really bad happened"
}
- Try and get a value from
flakeyApi
- If
flakeyApi
doesn’t return a successful value, trylessFlakeyApi
(.orElse
is a method ofTry
) - Transform the success value of either flakeyApi or lessFlakeyApi to an upper case String
- Finally,
flatMap
the result with anotherTry
Either Monad
So the above logic is all well and good, but the question is — “Why would I produce code that has side effects because of exceptions, only to wrap my functions with Try function invocations?”
Well that answer is you wouldn’t, the above example is good for when you’re trying to make non functional API’s that you don’t have any control over functional. For instance, working with an underlying Java package that has no notion of returning failure as a function value.
Scala has a built in Monadic type called Either
, similar to Try
it
can contain one of two values — a “Left value” or a “Right value”,
making it a great choice for returning failure or success to a caller.
Here’s an example when you have control over the API that you’re building —
package example
case class Person(name: String, age: Int)
case class Grade(subject: String, grade: Int)
sealed trait PersonException
case class InvalidPersonException(message: String) extends PersonException
object ExampleApp extends App {
def getGradesForPerson(person: Person): Either[PersonException, Grade] = person match {
case Person("Joe Bloggs", _) => Right(Grade("English", 88))
case Person("John Jones", _) => Right(Grade("Maths", 101))
case Person(name, _) => Left(InvalidPersonException(s"Invalid person $name"))
}
val joeBloggs = Person("Joe Bloggs", 33)
val johnJones = Person("John Jones", 29)
val unknownPerson = Person("Will Smith", 33)
getGradesForPerson(joeBloggs) // => Right(Grade(English,88))
getGradesForPerson(johnJones) // => Right(Grade(Maths,101))
getGradesForPerson(unknownPerson) // => Left(InvalidPersonException(Invalid person Will Smith))
}
As you can see here just by reading the function signature for getGradesForPerson
it can return an error or it can return a value. In the try/catch world you need to dig into the function body to see if exceptions can be thrown or not.
Similarly to Try
, Either
types also compose and there’s a few different flavours for how we can chain transformations from one Either
type to the next, below is an example with .flatMap
—
val joeBloggs = Person("Joe Bloggs", 33)
val result = getGradesForPerson(joeBloggs)
.flatMap(getGrade)
flatMap
is a method on Either
and we can transform from one Either
to another. Here we’re calling getGradesForPerson
which is an Either[PersonException, Grade]
and we want to end up with Either[GradeException, Int]
. getGrade
will only be called if the first Either
returns Right
. If it returns Left
then result will look something like: Left(GradeException)
As you can probably sense, if there are multiple function calls in addition this, you could end up with some tricky to read flatMap chains. A cleaner and usually more preferred approach is to use a for-comprehension like this:
val forResult = for {
grades <- getGradesForPerson(joeBloggs)
grade <- getGrade(grades)
} yield grade
Personally, I find the for comprehension the most readable. The for-comprehension is actually syntactic sugar and behind the scenes at compile time, this will translates to a flatMap
like the previous example.
But there’s a gotcha …
The type of forResult
is not Either[GradeException, Int]
like you might expect because it’s the last function call in the series. It’s actually Either[Object, Int]
and this caught me out so many times in the beginning when learning Scala. So let’s break this down:
- The return value for
getGradesForPerson
isEither[PersonException, Grade]
- We’re then flat mapping with
getGrade
which has a different error type. When I say type here, I mean the error traits we’ve defined:PersonException
andGradeException
- Scala sees that there are two potential types in play and depending on your Scala version will infer the type as
Object
orProduct with Serializable
So — when you’re tech designing what your functions will accept as inputs and return as an output, be sure that you don’t confuse things by returning different error types. Here’s a revised version with a single error trait that our case classes can inherit:
sealed trait GradeException
case class UnknownSubjectException(message: String) extends GradeException
case class InvalidGradeException(message: String) extends GradeException
case class InvalidStudentException(message: String) extends GradeException
def getGradesForPerson(person: Person): Either[GradeException, Grade] =
person match {
case Person("Joe Bloggs", _) => Right(Grade("English", 88))
case Person("John Jones", _) => Right(Grade("Maths", 101))
case Person(name, _) =>
Left(InvalidStudentException(s"Invalid person $name"))
}
def getGrade(grade: Grade): Either[GradeException, Int] = grade match {
case Grade(subject, age) if age > 100 =>
Left(InvalidGradeException(s"$subject grade cannot be greater than 100"))
case Grade(subject, age) if age < 0 =>
Left(InvalidGradeException(s"$subject grade cannot be less than 0"))
case Grade(_, grade) => Right(grade)
}
val johnJones = Person("John Jones", 33)
val maybeGrade: Either[GradeException, Int] = getGradesForPerson(johnJones)
.flatMap(getGrade)
Evaluating Either types
With Either
types, we need a clean way of inspecting values to see if we’re handling a Left
or Right
value. For this, we can use a simple pattern match expression as follows:
val result = getGradesForPerson(invalidPerson)
.flatMap(getGrade)
result match {
case Left(error) => println(error)
case Right(grade) => println(s"Got result: $grade")
}
What catch
blocks in try/catch really are
For completeness, if we go back to where we started and examine the try catch statement from before:
val traditionalTry: String =
try {
flakeyApi()
} catch {
case error: RuntimeException => "it did not work, but I recovered here!"
case _: Throwable => "Something really bad happened"
}
println(traditionalTry)
This catch statement is a pattern match expression which is exactly how we evaluated what type of value we were handling on the Either
type.
Final thoughts
What sold me on this approach is transparency — function signatures that don’t lie. When I see Either[GradeException, Grade]
, I know exactly what can go wrong and how to handle it. Yes, wrapping everything in Try
or Either
adds verbosity. But I’ll take explicit error handling over mysterious RuntimeExceptions any day.
Top comments (0)