Scala Exception Handling
Exception handling is an important guarantee for program robustness. Scala provides multiple exception handling mechanisms, including traditional try-catch, functional Try types, and modern error handling patterns.
Basic Exception Handling
try-catch-finally
scala
import scala.util.{Try, Success, Failure}
import java.io.{FileReader, BufferedReader, IOException}
object BasicExceptionHandling {
def main(args: Array[String]): Unit = {
// Basic try-catch
def divide(x: Int, y: Int): Double = {
try {
x.toDouble / y
} catch {
case _: ArithmeticException =>
println("Division by zero error")
0.0
case e: Exception =>
println(s"Other error: ${e.getMessage}")
0.0
}
}
println(s"10 / 2 = ${divide(10, 2)}")
println(s"10 / 0 = ${divide(10, 0)}")
// Exception handling with finally
def readFileWithFinally(filename: String): String = {
var reader: BufferedReader = null
try {
reader = new BufferedReader(new FileReader(filename))
reader.readLine()
} catch {
case _: IOException =>
println(s"Failed to read file $filename")
""
case e: Exception =>
println(s"Unknown error: ${e.getMessage}")
""
} finally {
if (reader != null) {
try {
reader.close()
} catch {
case _: IOException => println("Failed to close file")
}
}
}
}
// Handling multiple exception types
def parseAndProcess(input: String): Int = {
try {
val number = input.toInt
if (number < 0) throw new IllegalArgumentException("Number cannot be negative")
number * 2
} catch {
case _: NumberFormatException =>
println("Input is not a valid number")
0
case e: IllegalArgumentException =>
println(s"Parameter error: ${e.getMessage}")
0
case e: Exception =>
println(s"Unknown error: ${e.getMessage}")
0
}
}
val inputs = List("10", "-5", "abc", "20")
inputs.foreach(input => println(s"Processing '$input': ${parseAndProcess(input)}"))
}
}Throwing Exceptions
scala
object ThrowingExceptions {
// Custom exceptions
class InvalidAgeException(message: String) extends Exception(message)
class InsufficientFundsException(message: String, val balance: Double) extends Exception(message)
case class Person(name: String, age: Int) {
if (age < 0) throw new InvalidAgeException(s"Age cannot be negative: $age")
if (age > 150) throw new InvalidAgeException(s"Age cannot exceed 150: $age")
}
class BankAccount(private var balance: Double) {
def withdraw(amount: Double): Double = {
if (amount <= 0) {
throw new IllegalArgumentException("Withdrawal amount must be greater than 0")
}
if (amount > balance) {
throw new InsufficientFundsException(s"Insufficient funds, current balance: $balance", balance)
}
balance -= amount
balance
}
def getBalance: Double = balance
}
// Conditional exception throwing
def validateEmail(email: String): String = {
if (email == null || email.trim.isEmpty) {
throw new IllegalArgumentException("Email cannot be empty")
}
if (!email.contains("@")) {
throw new IllegalArgumentException("Invalid email format")
}
email.toLowerCase
}
def main(args: Array[String]): Unit = {
// Test custom exceptions
try {
val person1 = Person("Alice", 25)
println(s"Created successfully: $person1")
val person2 = Person("Bob", -5) // Will throw exception
} catch {
case e: InvalidAgeException =>
println(s"Age validation failed: ${e.getMessage}")
}
// Test bank account exceptions
val account = new BankAccount(1000.0)
try {
println(s"Balance before withdrawal: ${account.getBalance}")
account.withdraw(500.0)
println(s"Balance after withdrawing 500: ${account.getBalance}")
account.withdraw(600.0) // Will throw exception
} catch {
case e: InsufficientFundsException =>
println(s"Withdrawal failed: ${e.getMessage}")
println(s"Current balance: ${e.balance}")
case e: IllegalArgumentException =>
println(s"Parameter error: ${e.getMessage}")
}
// Test email validation
val emails = List("user@example.com", "", "invalid-email", null)
emails.foreach { email =>
try {
val validEmail = validateEmail(email)
println(s"Valid email: $validEmail")
} catch {
case e: IllegalArgumentException =>
println(s"Email validation failed: ${e.getMessage}")
case e: NullPointerException =>
println("Email is null")
}
}
}
}Functional Exception Handling
Try Type
scala
import scala.util.{Try, Success, Failure}
import scala.io.Source
import java.net.URL
object TryExceptionHandling {
// Use Try to wrap operations that might fail
def safeDivide(x: Double, y: Double): Try[Double] = {
Try(x / y)
}
def safeParseInt(str: String): Try[Int] = {
Try(str.toInt)
}
def safeReadFile(filename: String): Try[String] = {
Try {
val source = Source.fromFile(filename)
try {
source.mkString
} finally {
source.close()
}
}
}
def safeHttpRequest(url: String): Try[String] = {
Try {
val source = Source.fromURL(new URL(url))
try {
source.mkString
} finally {
source.close()
}
}
}
// Try chain operations
def processUserInput(input: String): Try[String] = {
for {
number <- safeParseInt(input)
doubled = number * 2
result <- Try(s"Result: $doubled")
} yield result
}
// Combine multiple Try operations
def calculateAverage(numbers: List[String]): Try[Double] = {
val parsedNumbers = numbers.map(safeParseInt)
// Check if all parsing succeeded
val allSuccess = parsedNumbers.forall(_.isSuccess)
if (allSuccess) {
val values = parsedNumbers.map(_.get)
Try(values.sum.toDouble / values.length)
} else {
Failure(new IllegalArgumentException("Contains invalid numbers"))
}
}
def main(args: Array[String]): Unit = {
// Basic Try usage
val division1 = safeDivide(10.0, 2.0)
val division2 = safeDivide(10.0, 0.0)
division1 match {
case Success(result) => println(s"Division successful: $result")
case Failure(exception) => println(s"Division failed: ${exception.getMessage}")
}
division2 match {
case Success(result) => println(s"Division successful: $result")
case Failure(exception) => println(s"Division failed: ${exception.getMessage}")
}
// Try functional operations
val numbers = List("10", "20", "abc", "30")
numbers.foreach { numStr =>
val result = safeParseInt(numStr)
.map(_ * 2)
.map(n => s"Doubled result: $n")
.recover {
case _: NumberFormatException => s"'$numStr' is not a valid number"
}
println(result.getOrElse("Processing failed"))
}
// Chain operations
val inputs = List("5", "abc", "10")
inputs.foreach { input =>
processUserInput(input) match {
case Success(result) => println(result)
case Failure(exception) => println(s"Processing failed: ${exception.getMessage}")
}
}
// Calculate average
val numberLists = List(
List("1", "2", "3", "4", "5"),
List("10", "20", "abc"),
List("100", "200", "300")
)
numberLists.foreach { numbers =>
calculateAverage(numbers) match {
case Success(avg) => println(s"Average: $avg")
case Failure(exception) => println(s"Calculation failed: ${exception.getMessage}")
}
}
}
}Option and Exception Handling
scala
object OptionExceptionHandling {
// Convert exceptions to Option
def safeGet[T](list: List[T], index: Int): Option[T] = {
try {
Some(list(index))
} catch {
case _: IndexOutOfBoundsException => None
}
}
def safeDivideOption(x: Double, y: Double): Option[Double] = {
if (y == 0) None else Some(x / y)
}
def safeParseIntOption(str: String): Option[Int] = {
try {
Some(str.toInt)
} catch {
case _: NumberFormatException => None
}
}
// Use Option for safe chain operations
def processChain(input: String): Option[String] = {
for {
number <- safeParseIntOption(input)
doubled <- Some(number * 2)
result <- if (doubled > 0) Some(s"Positive result: $doubled") else None
} yield result
}
// Combine multiple Option operations
def combineOptions(opt1: Option[Int], opt2: Option[Int]): Option[Int] = {
for {
a <- opt1
b <- opt2
} yield a + b
}
// Convert Try to Option
def tryToOption[T](t: Try[T]): Option[T] = t.toOption
def main(args: Array[String]): Unit = {
val list = List(1, 2, 3, 4, 5)
// Safe list access
println(s"Index 2: ${safeGet(list, 2)}")
println(s"Index 10: ${safeGet(list, 10)}")
// Safe division
println(s"10 / 2: ${safeDivideOption(10, 2)}")
println(s"10 / 0: ${safeDivideOption(10, 0)}")
// Chain operations
val inputs = List("5", "-3", "abc", "10")
inputs.foreach { input =>
processChain(input) match {
case Some(result) => println(result)
case None => println(s"Processing '$input' failed")
}
}
// Combine operations
val combinations = List(
(Some(1), Some(2)),
(Some(3), None),
(None, Some(4)),
(Some(5), Some(6))
)
combinations.foreach { case (opt1, opt2) =>
combineOptions(opt1, opt2) match {
case Some(sum) => println(s"$opt1 + $opt2 = $sum")
case None => println(s"Cannot calculate $opt1 + $opt2")
}
}
// Try to Option conversion
val tryResults = List(
Try("hello".toInt),
Try("123".toInt),
Try(10 / 0)
)
tryResults.foreach { t =>
val opt = tryToOption(t)
println(s"Try to Option: $opt")
}
}
}Resource Management
Automatic Resource Management
scala
import scala.util.{Try, Using}
import java.io.{FileWriter, BufferedWriter, FileReader, BufferedReader}
object ResourceManagement {
// Traditional resource management (error-prone)
def writeFileTraditional(filename: String, content: String): Try[Unit] = {
Try {
var writer: BufferedWriter = null
try {
writer = new BufferedWriter(new FileWriter(filename))
writer.write(content)
} finally {
if (writer != null) {
writer.close()
}
}
}
}
// Use Using for automatic resource management (Scala 2.13+)
def writeFileUsing(filename: String, content: String): Try[Unit] = {
Using(new BufferedWriter(new FileWriter(filename))) { writer =>
writer.write(content)
}
}
def readFileUsing(filename: String): Try[String] = {
Using(new BufferedReader(new FileReader(filename))) { reader =>
Iterator.continually(reader.readLine())
.takeWhile(_ != null)
.mkString("\n")
}
}
// Manage multiple resources
def copyFile(source: String, target: String): Try[Unit] = {
Using.Manager { use =>
val reader = use(new BufferedReader(new FileReader(source)))
val writer = use(new BufferedWriter(new FileWriter(target)))
Iterator.continually(reader.readLine())
.takeWhile(_ != null)
.foreach(line => writer.write(line + "\n"))
}
}
// Custom resource management
class DatabaseConnection {
println("Database connection established")
def query(sql: String): String = {
s"Execute query: $sql"
}
def close(): Unit = {
println("Database connection closed")
}
}
// Implement AutoCloseable for custom resource
class ManagedDatabaseConnection extends DatabaseConnection with AutoCloseable
def withDatabase[T](operation: DatabaseConnection => T): Try[T] = {
Using(new ManagedDatabaseConnection())(operation)
}
// Manual resource management pattern
def withResource[R <: AutoCloseable, T](resource: => R)(operation: R => T): Try[T] = {
Try {
val r = resource
try {
operation(r)
} finally {
r.close()
}
}
}
def main(args: Array[String]): Unit = {
val testFile = "test.txt"
val copyFile = "copy.txt"
val content = "Hello, World!\nThis is a test file.\nScala resource management."
// Write to file
writeFileUsing(testFile, content) match {
case scala.util.Success(_) => println("File written successfully")
case scala.util.Failure(exception) => println(s"File write failed: ${exception.getMessage}")
}
// Read from file
readFileUsing(testFile) match {
case scala.util.Success(fileContent) =>
println("File content:")
println(fileContent)
case scala.util.Failure(exception) =>
println(s"File read failed: ${exception.getMessage}")
}
// Copy file
copyFile(testFile, copyFile) match {
case scala.util.Success(_) => println("File copied successfully")
case scala.util.Failure(exception) => println(s"File copy failed: ${exception.getMessage}")
}
// Database operations
withDatabase { db =>
val result1 = db.query("SELECT * FROM users")
val result2 = db.query("SELECT * FROM orders")
List(result1, result2)
} match {
case scala.util.Success(results) =>
println("Database query results:")
results.foreach(println)
case scala.util.Failure(exception) =>
println(s"Database operation failed: ${exception.getMessage}")
}
}
}Scala provides multiple exception handling mechanisms:
Traditional Exception Handling:
- try-catch-finally
- Throw custom exceptions
- Suitable for Java interoperability
Functional Error Handling:
- Try type: Wrap operations that might fail
- Option type: Handle potentially null values
- Either type: Clear error messages
Resource Management:
- Using type: Automatic resource management
- Custom resource management patterns
- Ensure proper resource cleanup
Advanced Patterns:
- Error accumulation: Collect all validation errors
- Retry patterns: Handle temporary failures
- Graceful degradation: Provide fallback options
Choose the appropriate exception handling approach based on the specific scenario, with functional approaches typically being safer and more composable.