Written by: Michael Stemmler, Ingo Bandlow & Ralph Wermke
Published: 17 October 2016

The Problem

One of the requirements in the projects we are working on is to handle incoming requests in an asynchronous way. So Scala Futures are all around. Since Futures have map and flatMap function implemented they can be handled in an elegant way using For-Comprehensions.


Example 1

def valueA: Future[Int] = Future.successful(longLastingComputation) def valueB: Future[Int] = Future.successful(longLastingComputation) val sum: Future[Int] = for { a <- valueA b <- valueB } yield a + b

Dealing with failures is an important part of network management systems since NMS usually needs to communicate with lots of different peer like network nodes, databases and other services to get information. It is needed to return values to the consumer of the NMS services and also to deliver meaningful error notifications in case of failures. The Either type of the Scala language looked quite perfect for us to achieve this. An Either contains one value out of two different datatypes. In our case, it is the value itself (the right side of the Either) or an error (the left side.)

You may ask why not simply throw an exception in case of an error. For us there are 2 reasons:
First is that we want to distinguish two kinds of errors:

  1. Errors which are expected and generated explicitly by our code and which might be handled or recovered by the application code (for example: connection to network element lost, inconsistent configuration data from client application …)
  2. Errors which are really unexpected and usually come from 3rd party libraries(for example: Serialization error, Timeouts…)

For the first kind of errors, we use the left side of the Either. The type of errors which a function may return is part of the functions signature, checked at compile time and visible for the developer. The second kind of error is propagated via exception and lead to a failed future.

The other reason is that we try to match the functional programming paradigm as far as possible. To raise an exception means a side effect and would break the functional style in our opinion.

So at the end, the functions in our system return Futures which contain Either. This makes the code which shall handle such kind of data quite complicated. You have two levels which you need to evaluate. First the Future itself and then the nested Either. If you need to explain to other teams how to write application code in your system it would be a mess.

Example 2

def valueA: Future[Either[String, Int]] = Future.successful(Right(42)) def valueB: Future[Either[String, Int]] = Future.successful(Left("Don't know the answer to life, the universe and everything")) case class MyException(errorMessage:String) extends Exception val sum: Future[Int] = for { eitherA <- valueA a = eitherA match{ case Right(v) => v case Left(error) => throw MyException(error) } eitherB <- valueB b = eitherB match{ case Right(v) => v case Left(error) => throw MyException(error) } } yield a + b

Looks not nice, doesn't it? You can imagine that your application code will become complex if you need to do it in that way and also add error handling to it. This was not what we wanted. The goal was to have the possibility to write For-Comprehensions like in example 1.

We needed something which wraps the complexity of it and removes the error handling from the application code as far as possible. We started to search for some 3rd party libs but did not find anything which fits our needs. Therefore, we started to write it on our own and called it FutureOnEither.


Solution

FutureOnEither

A FutureOnEither is a Future which may be completed with Success, with an Error or a Failure.

  • Success means that the result of the future is a Right[_]
  • Error means that the future itself completes with success but the result was a Left[_]
  • Failure means that the future completes with an exception

You can think of it like a "right-biased" Future. It allows you to stack the execution of different function with an For-Comprehension without the need to take care about possible errors. As long as the FutureOnEither returned by a function completes with Success the next function is called. Otherwise the sequence will break and return an error.

Nevertheless, it is still possible to explicitly react on errors. You can map errors to other ones, recover errors to success values or recover errors by executing other functions, so it gives you full control over the error handling. But in the most cases it makes your life easier if you don't have to deal with errors returned by called functions.

It has the following signature:

class FutureOnEither[L, R](val future: Future[Either[L, R]])

Within the class we implemented the following functions. Most of them you will find also for Scala Future.

FunctionDescription

map[S](f: R => S): FutureOnEither[L, S]

Creates a new FutureOnEither by applying the function f to the result of this future if it completes with Success.

flatMap[S](f: R => FutureOnEither[L, S]): FutureOnEither[L, S]

Creates a new FutureOnEither by applying the function f to the result of this future if it completes with Success.

foreach[U](f: R => U): Unit

Asynchronously processes the Right-value of the contained Either once the future becomes available with Success.

filter(pred: R => Boolean): FutureOnEither[L, R]

Creates a new FutureOnEither by filtering the value of the current FutureOnEither with a predicate.

If the FutureOnEither contains a value which satisfies the predicate, the new FutureOnEither will also hold that value.
Otherwise, the resulting FutureOnEither will fail with a `NoSuchElementException`.
If the current FutureOnEither fails, then the resulting future also fails.

collect[S](pf: PartialFunction[R, S]): FutureOnEither[L, S]

Creates a new FutureOnEither by mapping the value of the current FutureOnEither, if the given partial function is defined at that value.

Otherwise, the resulting FutureOnEither will fail with a `NoSuchElementException`.

recover[L2 >: L, R2 >: R](pf: PartialFunction[Throwable, Either[L2,R2]]): FutureOnEither[L2, R2]

Recovers this FutureOnEither from a Failure. It creates a new FutureOnEither that will handle any matching throwable that this future might contain. If there is no match, or if this FutureOnEither contains a valid result then the new future will contain the same.

recoverError(pf: PartialFunction[L, R]): FutureOnEither[L, R]

Recover this FutureOnEither from an Error. Returns a new future which will handle any matching error (not throwable).

transform[L2,R2](s: Either[L, R] => Either[L2, R2], f: Throwable => Throwable): FutureOnEither[L2,R2]

Creates a new FutureOnEither by applying the function 's' to the Success result of this future, or the 'f' function to the failed result. If there is any non-fatal exception thrown when 's' or 'f' is applied, that exception will be propagated to the resulting FutureOnEither.

onComplete[U](f: Try[Either[L,R]] => U): Unit

Register a callback function called in case the FutureOnEither completes with Success or Error.

onFailure(pf: PartialFunction[Throwable, Unit]): Unit

Register a callback function called in case the FutureOnEither completes with a Failure.

onError(pf: PartialFunction[L, Unit]): FutureOnEither[L, R]

Register a callback function called in case the FutureOnEither completes with an Error.

Futor

In the systems in which our team is involved we usually use so called Operations. An operation is a request which is sent to a service or handed over to a function to be processed. It is defined as a Scala case class which contains the data needed to process the request. The outcome of the processing is a Result which can be either Success or Error. This is where our construct of Futor comes into the picture. The Futor is on one hand a type alias for FutureOnEither. The left side of the contained Either represents the Error case and contains an instance of Operation.Error. The right side contains the value which is the result of the operation in case of Success.

type Futor[T] = FutureOnEither[Operation.Error, T]

On the other hand the Futor is just an object with some functions we put on top of FutureOnEither.

FunctionDescription

apply[T](fe: => Future[Either[Operation.Error, T]]): Futor[T]

Create a Futor by applying the given Future[Either].

apply[T](either: Either[Operation.Error, T]): Futor[T]

Create a Futor by applying the given Either. The resulting Futor completes immediatly with either Success or Error depending on the given Either.

success[T](res: => T): Futor[T]

Create a Futor which completes with the given success result after the computation of 'res' is finished. If an exception is thrown during the computation the Futor will be finished with that Failure.

error[T](error: Operation.Error): Futor[T]

Create a Futor which immediatly completes with the given Error.

sequence[R](in: Seq[Futor[R]]): Futor[Seq[R]]

Convert a sequence of Futors into a Futor containing a sequence of results.

expectFalse(cond: => Boolean, err: => Operation.Error): Futor[Boolean]

Check that the given condition returns false once the Futor completes. If the condition returns true the successful completed Futor will be converted into a Futor which completes with Error.

expectTrue(cond: => Boolean, err: => Operation.Error): Futor[Boolean]

Check that the given condition returns true once the Futor completes. If the condition returns false the successful completed Futor will be converted into a Futor which completes with Error.

Some examples

Straight Forward Usage

private def deleteTableRow(rowId:Int):Futor[Unit] //sends request to network element to delete a row in a table private def queryTableRowIndex(row:String):Futor[Int] //query table row index by name from a database def processDeleteTableRow(rowName:String):Futor[CommandSuccess] = for { index <- queryTableRowIndex(rowName) _ <- deleteTableRow(index) } yield CommandSuccess

Assume we implement a function which handles a request to delete a specific row in a table from a network element. The consumer of the function only knows the name of the row while the row is identified in the network element by a number. The function first needs to handle the request in 2 steps. First map the row name to the row index, and then send a request to the network element to delete this row. Both steps are executed asynchron and in every step errors may occur or exceptions may be thrown. Nevertheless the function can be defined as a simple For-Comprehension. If both steps complete with Success our function will return a Futor which completes with a Success result. If a step could not complete successfully, our For-Comprehension will stop and return an error. As you can see, the error handling is done automatically and the developer does not need to explicitly take care of it.

Explicit Handling of Error

case class RowNotExist(ne: NeId, rowIndex:Int) extends Operation.Error //signature of the Error which may returned by the deleteTableRow function def processDeleteTableRow(rowName:String):Futor[CommandSuccess] = { val result = for { index <- queryTableRowIndex(rowName) _ <- deleteTableRow(index) } yield CommandSuccess result.recover { case RowNotExist(_, _) => CommandSuccess } }

In case the row in the table does not exist on the network element, the Futor returned by 'deleteTableRow' will complete with the Error 'RowNotExist'. For some reason we do not want to return this Error to the consumer, we just want to return Success since the row does not exist anymore. This can be achieved by using the 'recover' function and specifying the kind of errors we want to handle explicitly. If no Error occurs, 'recover' has no effect. If the Futor completes with an Error, the errors defined in the given partial function will be converted to Success. In case there is an Error which is not matched, the function 'processDeleteTableRow' will complete with this Error.

Control The Flow

case class RowNotDeleted(errorMsg:String, rowIndex:Int) extends Operation.Error def processDeleteTableRow(rowName:String):Futor[CommandSuccess] = for { index <- queryTableRowIndex(rowName) _ <- Futor.expectFalse(index == 0, RowNotDeleted("Not allowed to delete the first row in the table", index)) _ <- deleteTableRow(index) } yield CommandSuccess

Here you see an example with the 'expectFalse' function. It is some kind of control-structure so that you can break the For-Comprehension on your own. In the example above, we use it to make sure that the table row with index 0 will not be deleted.


Conclusion

In Scala, asynchronous programming and error handling can be achieved in an elegant way. You need to spend some time to learn about the concepts of Futures and Monads, but in the end you can write a handful of functions that fit to your needs and supports the application developer to write better code.

Often in the software industry, you can find different solution for the same problem. Here we want to mention the scalaZ library and the classes \ / (called disjunction) and EitherT.



About ADTRAN

ADTRAN, Inc. is a leading global provider of networking and communications equipment. ADTRAN’s products enable voice, data, video and Internet communications across a variety of network infrastructures. ADTRAN solutions are currently in use by service providers, private enterprises, government organizations, and millions of individual users worldwide. For more information, please visit www.adtran.com.