Scala - Try/Success/Failure
Scala's `Try` type represents a computation that may either result in a value (`Success`) or an exception (`Failure`). It's part of `scala.util` and provides a functional approach to error handling...
Key Insights
- Scala’s Try monad provides functional error handling without exceptions polluting your code flow, wrapping computations that may fail in Success or Failure containers
- Try eliminates try-catch blocks and enables composition of fallible operations through map, flatMap, recover, and other combinators that maintain referential transparency
- Pattern matching on Try results gives you explicit control over success and failure paths while keeping error handling logic close to business logic
Understanding Try as a Functional Alternative
Scala’s Try type represents a computation that may either result in a value (Success) or an exception (Failure). It’s part of scala.util and provides a functional approach to error handling that avoids the imperative nature of try-catch blocks.
import scala.util.{Try, Success, Failure}
def divide(a: Int, b: Int): Try[Int] = Try(a / b)
val result1 = divide(10, 2) // Success(5)
val result2 = divide(10, 0) // Failure(java.lang.ArithmeticException: / by zero)
The Try companion object’s apply method wraps any code block and catches non-fatal exceptions automatically. This makes it ideal for operations that interact with external systems, parse data, or perform calculations that might fail.
Basic Operations and Transformations
Try supports standard monadic operations that allow you to chain computations while maintaining error handling semantics.
import scala.util.Try
def parseInt(s: String): Try[Int] = Try(s.toInt)
def calculateSquare(n: Int): Try[Int] = Try(n * n)
// Using map to transform success values
val result = parseInt("42").map(_ * 2) // Success(84)
val failed = parseInt("abc").map(_ * 2) // Failure remains Failure
// Using flatMap to chain Try operations
val chained = parseInt("5").flatMap(calculateSquare) // Success(25)
// Using for-comprehension for multiple operations
val computation = for {
x <- parseInt("10")
y <- parseInt("20")
sum = x + y
squared <- calculateSquare(sum)
} yield squared // Success(900)
The key advantage here is that once a Failure occurs in a chain, subsequent operations are skipped, and the Failure propagates through the entire chain.
Pattern Matching on Try Results
Pattern matching provides explicit handling of both success and failure cases, giving you fine-grained control over error scenarios.
import scala.util.{Try, Success, Failure}
import scala.io.Source
def readFile(path: String): Try[String] = Try {
val source = Source.fromFile(path)
try source.mkString finally source.close()
}
readFile("config.txt") match {
case Success(content) =>
println(s"File content: $content")
case Failure(exception) =>
println(s"Failed to read file: ${exception.getMessage}")
}
// More specific pattern matching
def processFile(path: String): String = readFile(path) match {
case Success(content) if content.nonEmpty =>
s"Processed ${content.length} characters"
case Success(_) =>
"File is empty"
case Failure(_: java.io.FileNotFoundException) =>
"File not found"
case Failure(ex) =>
s"Error: ${ex.getClass.getSimpleName}"
}
Recovery and Fallback Strategies
Try provides several methods for recovering from failures or providing default values when operations fail.
import scala.util.Try
def fetchFromPrimaryDB(id: Int): Try[String] =
Try(throw new Exception("Primary DB down"))
def fetchFromSecondaryDB(id: Int): Try[String] =
Try(s"User data for $id from secondary")
// Using recover to handle specific exceptions
val recovered = Try("123".toInt / 0).recover {
case _: ArithmeticException => 0
} // Success(0)
// Using recoverWith to return another Try
val withFallback = fetchFromPrimaryDB(42).recoverWith {
case _: Exception => fetchFromSecondaryDB(42)
} // Success("User data for 42 from secondary")
// Using getOrElse for simple default values
val value = Try("abc".toInt).getOrElse(0) // 0
// Using orElse to try an alternative computation
val alternative = Try("abc".toInt).orElse(Try("42".toInt)) // Success(42)
Combining Multiple Try Operations
Real-world applications often need to combine results from multiple fallible operations.
import scala.util.Try
case class User(id: Int, name: String, age: Int)
def validateId(id: String): Try[Int] =
Try(id.toInt).filter(_ > 0)
def validateName(name: String): Try[String] =
if (name.trim.nonEmpty) Try(name)
else Try(throw new IllegalArgumentException("Name cannot be empty"))
def validateAge(age: String): Try[Int] =
Try(age.toInt).filter(a => a >= 0 && a <= 150)
// Sequential validation with for-comprehension
def createUser(id: String, name: String, age: String): Try[User] = for {
validId <- validateId(id)
validName <- validateName(name)
validAge <- validateAge(age)
} yield User(validId, validName, validAge)
// Usage
createUser("123", "Alice", "30") match {
case Success(user) => println(s"Created: $user")
case Failure(ex) => println(s"Validation failed: ${ex.getMessage}")
}
// Parallel validation collecting all errors
def validateUserParallel(id: String, name: String, age: String): Try[User] = {
val results = List(validateId(id), validateName(name), validateAge(age))
val failures = results.collect { case Failure(ex) => ex }
if (failures.isEmpty) {
for {
id <- validateId(id)
name <- validateName(name)
age <- validateAge(age)
} yield User(id, name, age)
} else {
Failure(new Exception(failures.map(_.getMessage).mkString(", ")))
}
}
Converting Between Try and Option
Try and Option are often used together, and Scala provides convenient conversion methods.
import scala.util.Try
def findUser(id: Int): Option[String] =
if (id > 0) Some(s"User$id") else None
// Option to Try
val tryFromOption: Try[String] =
findUser(42).fold[Try[String]](
Failure(new NoSuchElementException("User not found"))
)(Success(_))
// Try to Option (loses exception information)
val optionFromTry: Option[Int] = Try("42".toInt).toOption // Some(42)
val failedOption: Option[Int] = Try("abc".toInt).toOption // None
// Combining Option and Try
def safeLookup(map: Map[String, String], key: String): Try[Int] =
Try(map(key).toInt)
val config = Map("timeout" -> "30", "retries" -> "invalid")
val timeout = safeLookup(config, "timeout") // Success(30)
val retries = safeLookup(config, "retries") // Failure(NumberFormatException)
val missing = safeLookup(config, "maxSize") // Failure(NoSuchElementException)
Practical Example: HTTP Client with Retry Logic
Here’s a realistic example combining Try with retry logic and recovery strategies.
import scala.util.{Try, Success, Failure}
import scala.annotation.tailrec
case class HttpResponse(status: Int, body: String)
object HttpClient {
def get(url: String): Try[HttpResponse] = Try {
// Simulated HTTP call
if (url.contains("fail")) throw new Exception("Network error")
HttpResponse(200, s"Response from $url")
}
@tailrec
def getWithRetry(url: String, maxRetries: Int, delay: Int = 1000): Try[HttpResponse] = {
get(url) match {
case Success(response) => Success(response)
case Failure(ex) if maxRetries > 0 =>
Thread.sleep(delay)
getWithRetry(url, maxRetries - 1, delay * 2)
case Failure(ex) => Failure(ex)
}
}
def getWithFallback(primaryUrl: String, fallbackUrl: String): Try[HttpResponse] = {
getWithRetry(primaryUrl, 3).recoverWith {
case ex: Exception =>
println(s"Primary failed: ${ex.getMessage}, trying fallback")
getWithRetry(fallbackUrl, 2)
}
}
}
// Usage
val result = HttpClient.getWithFallback(
"https://api.primary.com/data",
"https://api.backup.com/data"
)
result match {
case Success(response) =>
println(s"Status: ${response.status}, Body: ${response.body}")
case Failure(ex) =>
println(s"All attempts failed: ${ex.getMessage}")
}
This pattern ensures robust error handling while maintaining clean, composable code. Try makes exception handling explicit in type signatures and enables functional composition that’s impossible with traditional try-catch blocks.