Home > Software design >  Error handling and recovery when making 2 future based calls
Error handling and recovery when making 2 future based calls

Time:10-07

I am making 2 calls that both return the following:

    def processUser(..): Future[Either[Error, UserStatus]]
    def processTransactions(..): Future[Either[Error, TransactionStatus]]

So I could do it like this:

    for {
       _ <- processUser(user)
       _ <- processTransaction(...)
    }

The 2 calls don't depend on each other, but processUser has to run before processTransaction.

Here are my rules:

  1. if processUser returns an error, don't call processTransaction
  2. if both succeed without an error, all is good
  3. if processUser returns successfully, but processTransaction fails with an error that I want to call unprocessUser.

How can I do this? I guess using a for comprehension is not well suited for this, what pattern is more ideal?

Note

I am not using cats library, just plain Scala 2.13.x with Futures.

CodePudding user response:

If you use Cats you can use something called a monad transformer, specifically EitherT for your case:

import cats.implicits._
import cats.data.EitherT

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

type User
type UserStatus
type Transaction
type TransactionStatus

def processUser(user: User): Future[Either[Error, UserStatus]] = ???
def processTransaction(tx: Transaction): Future[Either[Error, TransactionStatus]] = ???
def unprocessUser(user: User): Future[Either[Error, TransactionStatus]]  = ???

val user : User = ???
val tx : Transaction = ???
def resCatsFuture(): Future[Either[Error, Unit]] =
  (for {
    userStatus <- EitherT(processUser(user))
    txStatus <- EitherT(processTransaction(tx))
      .handleErrorWith(e => EitherT(unprocessUser(user)))
  } yield ()).value

And since we are on Cats you should also consider replacing Future with IO from Cats Effect:

import cats.effect._

def processUserIO(user: User): IO[Either[Error, UserStatus]] = ???
def processTransactionIO(tx: Transaction): IO[Either[Error, TransactionStatus]] = ???
def unprocessUserIO(user: User): IO[Either[Error, TransactionStatus]]  = ???

val resCatsIO: IO[Either[Error, Unit]] =
  (for {
    userStatus <- EitherT(processUserIO(user))
    txStatus <- EitherT(processTransactionIO(tx))
      .handleErrorWith(e => EitherT(unprocessUserIO(user)))
  } yield ()).value

Or with ZIO's typed errors as an alternative to monad transformers:

import zio._

def processUserZIO(user: User): ZIO[Any, Error, UserStatus] = ???
def processTransactionZIO(tx: Transaction): ZIO[Any, Error, TransactionStatus] = ???
def unprocessUserZIO(user: User): ZIO[Any, Error, TransactionStatus]  = ???

val resZIO: ZIO[Any, Error, Unit] =
  for {
    userStatus <- processUserZIO(user)
    txStatus <- processTransactionZIO(tx)
      .catchAll(e => unprocessUserZIO(user))
  } yield ()

If you want to stick to vanilla Scala you can use fold on the inner Either and return a new Future for both Left and Right cases so you can keep your for-comprehension going.

def unprocessUser2(user: User): Future[Transaction] = ???
def unprocessUser3(user: User): Future[Unit] = ???

val res: Future[Unit] =
  for {
    mUserStatus <- processUser(user)
    userStatus <- mUserStatus.fold(Future.failed, Future.successful) //you could also do `Future.fromTry(mUserStatus.toTry)` here
    mTxStatus <- processTransaction(tx)
    txStatus <- mTxStatus.fold(e => unprocessUser2(user), Future.successful)
    //or alternatively, depending on how you want to handle the `Left` and Right values
    _ <- mTxStatus.fold(e => unprocessUser3(user), tx => Future(println(tx)))
  } yield ()

I'm making a lot assumptions about how unprocessUser should work and where it should be handled. There are also other error handling methods in Cats and ZIO that might be better for your use case.

CodePudding user response:

for what you want, I think you don't need a for comprehension. Try this (please note that is writed dirrectly as an answer, can have some errors at compile)

processUser(user) map {
   _ => processTransaction(user) flatMap {
       _ => // do nothing
   } recover {
     case e => unprocessUser()
   }
} recover {
  case e => {
    // throw err
  }
}
  • Related