Skip to content

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:

  1. Traditional Exception Handling:

    • try-catch-finally
    • Throw custom exceptions
    • Suitable for Java interoperability
  2. Functional Error Handling:

    • Try type: Wrap operations that might fail
    • Option type: Handle potentially null values
    • Either type: Clear error messages
  3. Resource Management:

    • Using type: Automatic resource management
    • Custom resource management patterns
    • Ensure proper resource cleanup
  4. 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.

Content is for learning and research only.