Omid
Omid

Reputation: 1989

How to add type to error message on Future

I write to a database in a simple function that returns a Future of the list that has been written to the database.

def writeToDB(db:Database, items: List[(String,String)]) = {
   Future {
      db.writeAsync(items) // Returns Unit
      items
   }
}

val res: Future[List[(String,String)]] = writeToDB(someItems)

On Success I have access to the written items so I can report which items have been written. But on Failure, in case the database fails, I don't have access to the failed items (e.g. for retry, for reporting and ...).

Is there any way to capture the items in error, something like Either that has Success(items) and Failure(items)?

Upvotes: 0

Views: 177

Answers (1)

Levi Ramsey
Levi Ramsey

Reputation: 20591

Because Scala's Future is essentially an asynchronous Try, failure has to be encoded as a Throwable (most likely as an Exception). One way to encode the failure to write an item to the DB is via

case class DBFailedToWriteItem(items: Seq[(String, String)], cause: Throwable) extends Exception("Failed to write item to database", cause)

def writeToDB(db: Database, items: Seq[(String, String)]): Future[Seq[(String, String)]] =
  Future { db.writeAsync(items) }
    .map { _ => items }
    .recoverWith {
      case NonFatal(e) => Future.failed(DBFailedToWriteItem(items, e))
    }

.recoverWith allows you to transform a failed Future into an arbitrary (either failed or successful) Future. The scala.util.control.NonFatal extractor prevents it from transforming certain failures which it's not reasonable to transform (generally ones that indicate the JVM is irretrievably hosed); I forget if recoverWith and friends implicitly perform this exclusion, so the match might be a bit of "belt and suspenders".

That said, I'm curious regarding the interface exposed by Database. If db.writeAsync is async, it's kind of odd for it to return Unit without giving the opportunity to attach a callback. The API seems to imply that it will catch a failure to set up the write, but not a failure to complete the write.

EDIT to add: from a comment, db.writeAsync returns a CompletableFuture (which is also a CompletionStage, Java's future API does not observe the read/write separation that Scala's does). You can use scala-java8-compat's scala.compat.java8.FutureConverters to convert a CompletionStage into a Future.

sealed trait DBFailed

def writeToDB(db: Database, items: Seq[(String, String)]): Future[Either[DBFailed, Seq[(String, String)]]] = {
  import scala.compat.java8.FutureConverters.toScala

  toScala(db.writeAsync(items))
    .map { _ => Right(items) }
    .recover {
      case NonFatal(e) => Left(???)  // create an appropriate 
                                     //  DBFailed... you can also 
                                     //  pattern match on the 
                                     //  particular exception from 
                                     //  db.writeAsync
    }
}

Note that with Either the convention is for the failure case to be the left (first) subtype and the success to be the right. Since Scala 2.12, this convention is encouraged by right-biasing Either:

val either: Either[String, Int] = ???
val rightTransform: Either[String, Double] = either.map(_.toDouble + 42.0)
val leftTransform: Either[Option[Char], Int] = either.left.map( _.headOption)

Upvotes: 4

Related Questions