Skip to content
AgileLogics
5 min read

Error Handling: Exceptions and Try

Prerequisites: For-comprehensions

try, catch, finally

@main def exceptions(): Unit =
  def parse(s: String): Int =
    try s.trim.toInt
    catch
      case _: NumberFormatException => 0
    finally println(s"parsed '$s'")

  println(parse(" 42 "))
  println(parse("abc"))
parsed ' 42 '
42
parsed 'abc'
0
  • try is an expression — it returns the value of the try block or the matching catch case.
  • catch uses pattern matching on the exception type.
  • finally always runs — for cleanup — and doesn't affect the result.

Throw with throw IllegalArgumentException("message"). Scala has no checked exceptions: the compiler doesn't force you to catch anything.

Try: errors as values

Try[A] holds either Success(value) or Failure(exception), so failure becomes a value you can pass around and transform:

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

@main def tries(): Unit =
  val good = Try("42".toInt)
  val bad = Try("abc".toInt)
  println(good)
  println(bad)

  println(good.map(_ * 2).getOrElse(0))
  println(bad.map(_ * 2).getOrElse(0))

  bad match
    case Success(n) => println(s"got $n")
    case Failure(e) => println(s"failed: ${e.getMessage}")

  val total = for
    a <- Try("10".toInt)
    b <- Try("5".toInt)
  yield a / b
  println(total)
  println(total.flatMap(t => Try(t / 0)))
Success(42)
Failure(java.lang.NumberFormatException: For input string: "abc")
84
0
failed: For input string: "abc"
Success(2)
Failure(java.lang.ArithmeticException: / by zero)

Try catches non-fatal exceptions, so it's ideal for wrapping Java APIs or parsing code that throws. toOption converts it to an Option when you don't need the error.

When to use which

  • Exceptions — truly unexpected problems (bugs, a missing config file at startup), and at the edges of your program.
  • Try — wrapping code that throws, when you want to handle failure as a value.
  • Either — expected failures with your own error type (next lesson).