Why Try? - Scala Error Handling

Writing an application that performs smoothly when everything goes as expected solves only half of the problem. But making sure that the application behaves properly when encountering unexpected conditions is where it really gets challenging. You could say that having a good understanding of the domain helps you better address the other half, but only if your language helps you support it. TLDR; at the end.

Throw that exception!

Imagine a developer trying to begin work at the start of the day.

Due to an overwhelming amount of bugs introduced by sleepy developers, the company has mandated the following check on all laptops and their users.

case class Developer ( var hadCoffee : Boolean ) class PreferredIDE case class CoffeeNotConsumedException ( message : String ) extends Exception ( message ) def openIDE ( developer : Developer ) : PreferredIDE ={ if ( developer . hadCoffee == false ) throw CoffeeNotConsumedException ( "Stop! You can't think, when you haven't had your coffee!" ) else { println ( "Get that data, you caffeinated developer!" ) new PreferredIDE } }

Now, frustrated developers need to be able to handle such an exception.

It’s the only way to access their coveted Preferred IDE.

Luckily, the developers were aware of Scala’s (Java’s) generic try/catch and groggily wrote the exception handling.

def haveCoffee ( developer : Developer ) : Developer = { println ( "Fine! Ingesting caffeine.. *gulp* *gulp* *gulp*" ) developer . hadCoffee = true developer } val sleepyShantanu = Developer ( false ) try { openIDE ( sleepyShantanu ) } catch { // Just for fun instead of simply catching and throwing. case CoffeeNotConsumedException ( msg ) => println ( msg ) openIDE ( haveCoffee ( sleepyShantanu )) }

Voila! Shantanu is now awake and ready to code.

And just in case, the coffee did not work..

Well if this works, why does scala.util.Try exist?

The try/catch implementation in Scala is exactly similar to java which makesit is not in sync with the unique demands of Scala. For this purpose, there was a need to create scala.util.Try which is a Monad.

Now let’s discuss the need for Try in detail and how it provides an alternative experience. I will highlight features that are unique to Try and how it can be essential in the certain use-cases of Scala.

At the end of this, I would like the take away to be that Scala developers should consider using Try instead of try/catch in most cases.

Try what?

The Try Monad represents a computation that may either result in an exception, or return a successfully computed value.

If you have an instance of type Try[A] , as successful computation would result in Success[A] where the value of A would be the ouput of the computation. An unsuccessful computation, would result in Failure[A] which actually wraps a Throwable which is any kind of NonFatal exception. It is important to note that, Try only wraps NonFatal exceptions. Any Fatal exceptions such as those mentioned earlier, are unhandled and cause the program to fail irrevocably, as it rightly should. The Try type has a number of useful methods that help the developer to handle the situation as seen fit.

Lets look at an example.

import scala.io.Source import scala.util. { Try , Success , Failure } // Reads a data file and returns the content as a Try[List[String]] def readTextFile ( filename : String ) : Try [ List [ String ]] = { Try ( Source . fromFile ( filename ). getLines . toList ) }

The above method can be handled as such:

val nullFile : Try [ List [ String ]] = readTextFile ( null ) nullFile match { case Success ( value ) => value . foreach ( println ) case Failure ( exception ) => println ( "Failed to get file" ) throw exception }

Or better yet, if you have a known default value.

// Because who doesn't have a README.md file everywhere... val dataFile = readTextFile ( null ). getOrElse ( readTextFile ( "README.md" ))

Fatality!

try { // Some dangerous code. throw new OutOfMemoryError ( "Something similar to - The building is on fire!!!" ) } catch { case e : Exception => log . error ( "Ehh, not a big deal. Let's just go about doing our own thing." ) //or case _ => log . error ( "Ehh, not a big deal. Let's just go about doing our own thing." ) }

The try/catch - as in java and scala - allows for an match all case which catches all exceptions and errors. These include Fatal Exceptions such as : OutOfMemoryError, InterruptedException, StackOverFlowExceptions etc. You may think of an Error as a Fatal Exception . It usually never makes sense to even try and recover from this because there is hardly anything you can do to handle these within your application. Catching of such conditions, is considered a bad practice.

Try on the other hand, only allows you to handle NonFatal exceptions, while Fatal exception rightfully cause the program to crash.

val buildingStatus = Try { // Some dangerous code. throw new OutOfMemoryError ( "The building is on fire!!!" ) // Program crash. The fire departments are alerted. There is nothing for you to do. }

Using Try prevents unknowingly these situations. It internally check if the Throwable is Fatal or NonFatal.

You don’t get it.

Now the problem with try/catch and concurrency. When using Future , the exception may be thrown on a different thread than the caller, and so can’t be returned through the stack. You never really get the exception. try/catch have no capabilities to encapsulates this exception and carry it forward to the caller.

If only there was a way to return an exception on failure and the result on success. Oh wait!

With Try, an exception on a separate thread can be wrapped up as Failure[A] and sent up the stack to the caller and hence at least notify the existence of an exception and the stack trace. The callback method basically has a return type of Try[A] and provides successful results in the form of Success[A] .

class Tweets ( id : String , text : String ) def getSensibleTweetsFrom ( twitterHandle : String ) : List [ Tweets ] = { // Parse huge amount Twitter data // Find the Tweets for the Twitter handle // Perform quick "sensibility recognition" and filter } val result = Future { // This could take some time... getSensibleTweetsFrom ( "realDonaldTrump" ) } result . onComplete { case Success ( value ) => if ( value . length >= 1 ) println ( "Impossible." ) else println ( "Not even one." ) case Failure ( exception ) => println ( "Need help recovering from the trauma of going through all that." ) throw exception }

Now you get it!

Hey you!

Yes you!

Do you keep referring to code documentation and sort through javadocs to check if a particular function throws an exception?

Do you forget to check and assume that no exception is thrown by the function call?

Do you even Error Handle bruhh?

Traditionally a function either returns the result or throws an exception. But you will never know if it can or will throw an exception. Well not until its too late anyway. There was no proper way to alert the caller that it has to handle the possible exception. But by simply setting the return type of a function as a Try , you force the developer to handle the possible exceptions. You start the error handling process. There by adding to the essence of graceful error handling.

The good, the ugly and the bad.

(Subjective to the kind of functional programming follower you are) Besides the issues mentioned above, the try/catch code all over the code base adds a lot of verbosity and makes the code look ugly. .

But Try adds to the logic of only two possible outcomes. As done in monads such as Either and Option. They provide and adapt the general syntactic sugar of the code.

Now, having said all this ‘try/catch’ is not bad at all. Try itself implements try/catch within its source code, for the sole reason of making the life of a developer better. But one can’t help but think about the additional overhead it brings in when working with Big Data. And yes, this might be a valid concern and generally comes down to cost vs benefit analysis. But on a side note, if you were wondering:

Try has no finally and it doesn’t really need it.

The Magician’s code.

Try enables a new set of magic within scala. Using Try provides you with the ability to use functions such as map, flatMap, filter etc. which are available in collections.

Mapping a Try[A] which has a Success[A] to a Try[B] results in a Success[B]. If map is performed on Failure[A] , the resulting Try[B] will be a Failure[B] but, containing the same exception as the Failure[A] .

Examples of composing on these exceptions by using try is:

def squareAndHalf ( number : String ) : Try [ Int ] = { Try ( number . toInt ). map ( value => value * value ). map ( value => value / 2 ) }

val result = for ( v <- Try ( "5" . toInt ); k <- Try ( "6" . toInt ); z <- Try ( "9" . toInt ) ) yield ( v + k + z )

These code blocks will execute until each operation results in a Success[A] and would return a Failure[A] when the corresponding operation returning an A fails. With Try, you can also enjoy the syntactic sugar of for comprehensions.

import scala.io.Source import java.net.URL import scala.util.Try def parseURL ( url : String ) : Try [ URL ] = Try ( new URL ( url )) def getURLContent ( url : String ) : Try [ Iterator [ String ]] = for { url <- parseURL ( url ) connection <- Try ( url . openConnection ()) is <- Try ( connection . getInputStream ) source = Source . fromInputStream ( is ) } yield source . getLines () // which is translated introduced def getURLContent ( url : String ) : Try [ Iterator [ String ]] = parseURL ( url ). flatMap ( url => Try ( url . openConnection ()). flatMap ( connection => Try ( connection . getInputStream ). map ( is => { val source = Source . fromInputStream ( is ) source . getLines () } ) ) )

That’s all I have from my side folks…

Before you go…

If you have time, watch the below video. It explains how to use Try, Option, Either or any other Monad and all its magic.

As seen above, you should enter the world of options only once and stay there until required to do so.

TLDR;

Just use Try. It’s good. Trust me, I’m an engineer.