home search about rss feed twitter ko-fi

FEH: Try (Scala 3 Video)

Introduction: Using Try instead of Option

Now that you’ve seen how Option works, let’s look at Try as an alternative to Option.

Try is almost the same as Option, but it lets you return a more meaningful error value. This helps the function caller understand what went wrong inside the function.

Just as Option has two sub-types, Try has two sub-types — Success and Failure — and the two sets of data types compare like this:

Option  <=>  Try
Some    <=>  Success
None    <=>  Failure

So basically anywhere you used Option before, you can also use Try. And where you used Some you now use Success, and instead of None you use Failure.

But the big difference is that when things go wrong, Failure contains an exception — the exception that occured inside the function. This is your function’s way to tell callers exactly what went wrong inside the function. Because of this, Try is generally more useful than Option when working with I/O functions.

Let’s see what this looks like.

Try/Success/Failure

Here’s a new version of makeInt that uses Try instead of Option:

// you must first import these types:
import scala.util.{Try, Success, Failure}

def makeInt(s: String): Try[Int] = 
    try
        Success(s.toInt)
    catch
        case e: NumberFormatException => Failure(e)

Now when you call makeInt, you’ll see these results in the success and failure cases:

// success case
makeInt("1")      // Success(1)

// failure case
makeInt("one")    // Failure(java.lang.NumberFormatException: 
                  //         For input string: "one")

As shown:

  • You get a Success back when the conversion works. Success is a wrapper that contains the Int.
  • You get a Failure back when the process fails. Failure contains the exception message.

As a consumer of the function, the Failure case is now much more interesting, because it tells me what went wrong inside the function.

Handling the result

As with Option, you handle a Try with a match expression and for expressions.

pattern matching

This is what Try looks like with a match expression:

makeInt(aString) match
    case Success(i) => println(s"Success: i = $i")
    case Failure(e) => println(s"Failed: msg = ${e.getMessage}")

for expressions

And this is what Try looks like when used with multiple strings that can be converted to integers in a for expression:

val maybeSum: Try[Int] = 
    for
        i <- makeInt("1")
        j <- makeInt("2")
        k <- makeInt("3")
    yield
        i + j + k

// result:  maybeSum == Success(6)

and this is what it looks like when they can’t be converted:

val maybeSum: Try[Int] = 
    for
        i <- makeInt("1")
        j <- makeInt("Hi mom!")
        k <- makeInt("3")
    yield
        i + j + k

// result:
maybeSum == Failure(java.lang.NumberFormatException: 
                    For input string: "Hi mom!")

And again — just like Option — whether you get a Success or Failure back from the for expression, you handle its result in a match expression:

maybeSum match
    case Success(i) => println(s"Success: i = $i")
    case Failure(e) => println(s"Failed: msg = ${e.getMessage}")

As shown, Try and Option are used in the same ways.

A shorter version of that function

I’ll show later how you can write more concise versions of makeInt with Option, Try, and Either, but Try is a little unique in that you can simply pass your exception-throwing algorithm into Try’s constructor:

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

def makeInt(s: String): Try[Int] = Try(s.toInt)

Because Try returns the exception that your code can throw, its creators realized that there was no reason to make you write a try/catch expression in the body of your function. You can just wrap your exception-throwing code inside Try as shown, and if that exception is thrown, it will catch it for you, and return a Failure.

Similarly, if your exception-throwing algorithm requires multiple lines of code, just write it like this, using curly braces around the block of code:

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

def makeInt(s: String): Try[Int] = Try {
    // some long
    // algorithm here
    s.toInt
}

Lastly, if you’re familiar with Scala, you’ll know that what I’m showing may be (a) Try’s constructor, or it may be (b) an apply method in Try’s companion object. But however it’s implemented, the great thing for you — the end user of its API — is that you write your code the same way.

Pure functions cannot lie

Before moving on, I’ll again reiterate this about the signatures of pure functions: they cannot lie.

As shown in this example, the makeInt function screams loud and clear with Try, “I’ll try to give you an Int if everything goes well, but something can wrong here”:

def makeInt(s: String): Try[Int] = ???
                        --------

Summary: Whenever you see Try (or Option) as the return type of a function, you know that something can go wrong inside that function.

Update: All of my new videos are now on
LearnScala.dev