Scala Exception Handling

Exception handling lets your program respond to errors gracefully instead of crashing. Scala inherits Java's exception model — you use try, catch, and finally — but Scala also provides functional alternatives like Try, Either, and Option that treat errors as values. This topic covers both approaches.

try / catch / finally

try
  val result = 10 / 0
  println(result)
catch
  case e: ArithmeticException =>
    println(s"Math error: ${e.getMessage}")
  case e: Exception =>
    println(s"General error: ${e.getMessage}")
finally
  println("This always runs, error or not")

// Math error: / by zero
// This always runs, error or not

try block
  │
  ├── Success → continues normally → finally block
  │
  └── Exception thrown
        │
        ├── catch case matches? → handle it → finally block
        └── no match → exception propagates up → finally block

try as an Expression

Like everything in Scala, try/catch is an expression and returns a value:

val result: Int =
  try "42".toInt
  catch
    case _: NumberFormatException => -1

println(result)   // 42

val bad: Int =
  try "abc".toInt
  catch
    case _: NumberFormatException => -1

println(bad)   // -1

Catching Multiple Exception Types

def readData(input: String): Int =
  try
    val num = input.trim.toInt
    require(num > 0, "Number must be positive")
    num
  catch
    case _: NumberFormatException =>
      println(s"'$input' is not a number")
      0
    case e: IllegalArgumentException =>
      println(s"Validation failed: ${e.getMessage}")
      0
    case e: Exception =>
      println(s"Unexpected: ${e.getClass.getSimpleName}")
      0

println(readData("42"))    // 42
println(readData("abc"))   // 'abc' is not a number → 0
println(readData("-5"))    // Validation failed: requirement failed → 0

finally — Guaranteed Cleanup

def processFile(filename: String): Unit =
  var opened = false
  try
    println(s"Opening $filename")
    opened = true
    if filename.endsWith(".bad") then
      throw new RuntimeException("Corrupt file!")
    println("File processed successfully")
  catch
    case e: RuntimeException =>
      println(s"Error: ${e.getMessage}")
  finally
    if opened then println(s"Closing $filename")

processFile("data.csv")
// Opening data.csv
// File processed successfully
// Closing data.csv

processFile("data.bad")
// Opening data.bad
// Error: Corrupt file!
// Closing data.bad

Throwing Exceptions

def divide(a: Int, b: Int): Int =
  if b == 0 then throw new ArithmeticException("Cannot divide by zero")
  a / b

def requirePositive(n: Int): Int =
  require(n > 0, s"Expected positive, got $n")  // throws IllegalArgumentException
  n

def fetchUser(id: Int): String =
  if id <= 0 then throw new IllegalArgumentException(s"Invalid ID: $id")
  s"User_$id"

Custom Exceptions

class InsufficientFundsException(val amount: Double, val available: Double)
    extends Exception(f"Cannot withdraw ₹$amount%.2f, only ₹$available%.2f available"):
  def shortfall: Double = amount - available

class AccountNotFoundException(val accountId: String)
    extends Exception(s"Account '$accountId' not found")

def withdraw(accountId: String, amount: Double): Unit =
  val balance = 500.0  // simulated
  if accountId.isEmpty then
    throw AccountNotFoundException(accountId)
  if amount > balance then
    throw InsufficientFundsException(amount, balance)
  println(f"Withdrew ₹$amount%.2f")

try withdraw("ACC001", 1000.0)
catch
  case e: InsufficientFundsException =>
    println(s"${e.getMessage} (shortfall: ₹${e.shortfall}%.2f)")
  case e: AccountNotFoundException =>
    println(e.getMessage)

// Cannot withdraw ₹1000.00, only ₹500.00 available (shortfall: ₹500.00)

Functional Error Handling — Try

import scala.util.{Try, Success, Failure}

def safeDiv(a: Int, b: Int): Try[Int] = Try(a / b)

safeDiv(10, 2) match
  case Success(v) => println(s"Result: $v")
  case Failure(e) => println(s"Error: ${e.getMessage}")
// Result: 5

val chain = Try("100".toInt).map(_ * 2).map(_ + 50)
println(chain)   // Success(250)

val bad = Try("abc".toInt).map(_ * 2)
println(bad)     // Failure(NumberFormatException: ...)

When to Use Which Approach


Situation                               Approach
──────────────────────────────────────  ──────────────────────────
Calling Java code that throws           try/catch or Try { }
Need guaranteed cleanup (I/O)           try/finally
Error carries no extra info needed      Option
Error carries a message or code         Either[String, A]
Exception-throwing code as value        Try[A]
Multiple error types to handle          try/catch with cases

Chaining with recover

import scala.util.Try

val result = Try("not-a-number".toInt)
  .recover { case _: NumberFormatException => 0 }
  .map(_ + 100)

println(result)   // Success(100)

// recoverWith returns another Try
val result2 = Try("bad".toInt)
  .recoverWith { case _ => Try("42".toInt) }

println(result2)   // Success(42)

Do Not Swallow Exceptions

// BAD — hides all errors silently
try doSomething()
catch case _: Exception => ()  // Don't do this!

// BETTER — log and handle
try doSomething()
catch case e: Exception =>
  println(s"Error in doSomething: ${e.getMessage}")
  // and/or rethrow, return a default, or use Try/Either

Silent exception swallowing makes debugging nearly impossible. Always log, handle, or propagate exceptions so failures are visible and traceable.

Leave a Comment

Your email address will not be published. Required fields are marked *