Scala - Either/Left/Right with Examples
Either[A, B] is an algebraic data type that represents a value of one of two possible types. It has exactly two subtypes: Left and Right. By convention, Left represents failure or error cases while...
Key Insights
- Either represents a value of one of two possible types (a disjoint union), conventionally used for error handling where Left contains error information and Right contains success values
- Unlike Option which only signals absence, Either preserves error context, making it superior for validation chains and complex error scenarios where you need to communicate what went wrong
- Either is right-biased in Scala 2.12+, meaning map, flatMap, and for-comprehensions operate on Right values by default, treating Left as a short-circuit failure case
Understanding Either Fundamentals
Either[A, B] is an algebraic data type that represents a value of one of two possible types. It has exactly two subtypes: Left and Right. By convention, Left represents failure or error cases while Right represents success.
sealed trait Either[+A, +B]
case class Left[+A](value: A) extends Either[A, Nothing]
case class Right[+B](value: B) extends Either[Nothing, B]
The key difference between Either and Option is that Either carries information about failures. While Option[T] can only tell you “something went wrong,” Either[Error, T] tells you “this specific error occurred.”
def divide(a: Int, b: Int): Either[String, Int] = {
if (b == 0) Left("Division by zero")
else Right(a / b)
}
val result1 = divide(10, 2) // Right(5)
val result2 = divide(10, 0) // Left("Division by zero")
Pattern Matching with Either
The most straightforward way to work with Either is pattern matching:
def parseAge(input: String): Either[String, Int] = {
try {
val age = input.toInt
if (age < 0) Left("Age cannot be negative")
else if (age > 150) Left("Age seems unrealistic")
else Right(age)
} catch {
case _: NumberFormatException => Left(s"'$input' is not a valid number")
}
}
val age = parseAge("25")
age match {
case Right(value) => println(s"Valid age: $value")
case Left(error) => println(s"Error: $error")
}
Right-Biased Operations
Since Scala 2.12, Either is right-biased, meaning operations like map and flatMap work on the Right value:
def validateUsername(name: String): Either[String, String] = {
if (name.isEmpty) Left("Username cannot be empty")
else if (name.length < 3) Left("Username too short")
else Right(name)
}
def validateEmail(email: String): Either[String, String] = {
if (email.contains("@")) Right(email)
else Left("Invalid email format")
}
// map operates on Right values
val result = validateUsername("john").map(_.toUpperCase)
// result: Right("JOHN")
val failed = validateUsername("").map(_.toUpperCase)
// failed: Left("Username cannot be empty") - map not applied
Chaining Operations with flatMap
flatMap allows you to chain Either-returning operations, short-circuiting on the first Left:
case class User(username: String, email: String, age: Int)
def validateUser(username: String, email: String, ageStr: String): Either[String, User] = {
for {
validUsername <- validateUsername(username)
validEmail <- validateEmail(email)
validAge <- parseAge(ageStr)
} yield User(validUsername, validEmail, validAge)
}
// Success case
validateUser("john_doe", "john@example.com", "30")
// Right(User(john_doe,john@example.com,30))
// Failure case - stops at first error
validateUser("jo", "john@example.com", "30")
// Left("Username too short")
Accumulating Errors with Validated
Either short-circuits on the first error. When you need to accumulate all errors, use cats.data.Validated:
import cats.data.ValidatedNec
import cats.implicits._
type ValidationResult[A] = ValidatedNec[String, A]
def validateUserAccumulating(
username: String,
email: String,
ageStr: String
): ValidationResult[User] = {
(
validateUsername(username).toValidatedNec,
validateEmail(email).toValidatedNec,
parseAge(ageStr).toValidatedNec
).mapN(User)
}
validateUserAccumulating("", "invalid-email", "not-a-number")
// Invalid(NonEmptyChain(
// "Username cannot be empty",
// "Invalid email format",
// "'not-a-number' is not a valid number"
// ))
Converting Between Either and Other Types
Either provides several conversion methods:
val right: Either[String, Int] = Right(42)
val left: Either[String, Int] = Left("error")
// Either to Option (loses error information)
right.toOption // Some(42)
left.toOption // None
// Either to Try
right.toTry // Success(42)
left.toTry // Failure(NoSuchElementException: Either.left.get on Right)
// Option to Either
val some: Option[Int] = Some(42)
val none: Option[Int] = None
some.toRight("No value present") // Right(42)
none.toRight("No value present") // Left("No value present")
// Try to Either
import scala.util.{Try, Success, Failure}
Success(42).toEither // Right(42)
Failure(new Exception("error")).toEither // Left(java.lang.Exception: error)
Practical Example: Configuration Parsing
Here’s a realistic example parsing configuration with detailed error reporting:
case class DatabaseConfig(host: String, port: Int, database: String)
object ConfigParser {
def getEnv(key: String): Either[String, String] = {
sys.env.get(key).toRight(s"Missing environment variable: $key")
}
def parsePort(portStr: String): Either[String, Int] = {
try {
val port = portStr.toInt
if (port > 0 && port < 65536) Right(port)
else Left(s"Port $port out of valid range (1-65535)")
} catch {
case _: NumberFormatException =>
Left(s"Invalid port number: $portStr")
}
}
def loadDatabaseConfig(): Either[String, DatabaseConfig] = {
for {
host <- getEnv("DB_HOST")
portStr <- getEnv("DB_PORT")
port <- parsePort(portStr)
database <- getEnv("DB_NAME")
} yield DatabaseConfig(host, port, database)
}
}
// Usage
ConfigParser.loadDatabaseConfig() match {
case Right(config) =>
println(s"Connecting to ${config.host}:${config.port}/${config.database}")
case Left(error) =>
System.err.println(s"Configuration error: $error")
sys.exit(1)
}
Advanced Pattern: Bimap and Fold
Transform both sides of an Either or collapse it into a single value:
val result: Either[String, Int] = divide(10, 2)
// Transform both Left and Right
val transformed = result.bimap(
error => s"Error occurred: $error",
value => value * 2
)
// Fold into a single value
val message = result.fold(
error => s"Failed: $error",
value => s"Result: $value"
)
// getOrElse provides default for Left
val value = divide(10, 0).getOrElse(0) // 0
Error Handling with Custom Error Types
Using sealed traits for errors provides type-safe error handling:
sealed trait ValidationError
case class EmptyField(field: String) extends ValidationError
case class InvalidFormat(field: String, reason: String) extends ValidationError
case class OutOfRange(field: String, min: Int, max: Int) extends ValidationError
def validateAge(age: Int): Either[ValidationError, Int] = {
if (age < 0) Left(OutOfRange("age", 0, 150))
else if (age > 150) Left(OutOfRange("age", 0, 150))
else Right(age)
}
validateAge(-5) match {
case Right(age) => println(s"Valid: $age")
case Left(OutOfRange(field, min, max)) =>
println(s"$field must be between $min and $max")
case Left(error) => println(s"Validation error: $error")
}
Either provides a robust foundation for error handling in Scala applications. Its right-biased nature makes it ergonomic for success-path programming while preserving complete error context. Use Either when you need to communicate failure reasons, and consider Validated when you need to accumulate multiple errors.