One of the core ideas in functional programming is that errors are just values. Instead of throwing exceptions, we make errors explicit in the type system, which leads to safer and more predictable code.
In Scala, a popular tool for this is the Either type.
Either[A, B] represents a value of one of two possible types: Left[A]: typically used to represent an error or failure, Right[B]: typically used to represent a success or valid result. By convention, Right is used for success and Left for failure.
def parseInt(str: String): Either[String, Int] = {
try {
Right(str.toInt)
} catch {
case _: NumberFormatException =>
Left(s"'$str' is not a valid integer")
}
}
Now instead of throwing, parseInt returns a value that explicitly tells the caller whether the operation succeeded or failed.
The power of Either shines when you start composing multiple operations. Suppose we want to read a string, parse it to an integer, and then divide 100 by that number.
def divide100By(n: Int): Either[String, Int] = {
if (n == 0) Left("Division by zero")
else Right(100 / n)
}
def process(input: String): Either[String, Int] = {
for {
number <- parseInt(input)
result <- divide100By(number)
} yield result
}
This uses a for-comprehension to cleanly chain operations. If any step fails, the computation short-circuits and returns the first Left.
Using Either encourages functions to be pure and makes failure cases part of the function's contract. In more complex systems, you'll often want to define your own error types.
sealed trait AppError
case class ParseError(input: String) extends AppError
case object DivisionByZero extends AppError
def parse(input: String): Either[AppError, Int] = {
try {
Right(input.toInt)
} catch {
case _: NumberFormatException => Left(ParseError(input))
}
}
def divide(a: Int): Either[AppError, Int] = {
if (a == 0) Left(DivisionByZero)
else Right(100 / a)
}
Now your errors are typed and structured, which is incredibly helpful for testing, logging, and handling different failure modes gracefully.
Exceptions are side-effects and make reasoning about code harder. They can happen anywhere, aren't reflected in function types, and must be caught at runtime.
Using Either: makes error handling explicit, works well with pure functions, encourages thoughtful error modeling, plays nicely with functional combinators like .map, .flatMap, .fold, etc.
val result: Either[String, Int] = Right(42)
result.map(_ * 2) // Right(84)
result.left.map(_.toUpperCase) // Right(42) (no-op in this case)
result.getOrElse(0) // 42
result.fold(
err => s"Error: $err",
value => s"Success: $value"
)
By treating errors as values with Either, Scala empowers you to write robust, predictable code. It aligns perfectly with functional programming principles—favoring purity, composition, and strong typing.
So next time you’re tempted to throw an exception, consider whether a typed Either might be a better choice. It just might save you from a debugging headache later.