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 theInt
. - 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