Unsucking Error Handling with Futures

39
Unsucking Errors with Futures Gary Coady <[email protected]>

Transcript of Unsucking Error Handling with Futures

Page 1: Unsucking Error Handling with Futures

Unsucking Errors with FuturesGary Coady <[email protected]>

Page 2: Unsucking Error Handling with Futures

Option[A]Error Handling: Possible Absence of a Value

Page 3: Unsucking Error Handling with Futures

def  userForEmail(email:  String):  Option[User]  =  ???  def  shoeSizeForUser(user:  User):  Option[Size]  =  ???  def  recommendedShoeStyleForSize(size:  Size):  Option[ShoeStyle]    =  ???  

def  recommendedShoe(email:  String):  Option[ShoeStyle]  =  for  {      user  <-­‐  userForEmail(email)      size  <-­‐  shoeSizeForUser(user)      shoe  <-­‐  recommendedShoeStyleForSize(size)  }  yield  shoe

recommendedShoe("[email protected]")  match  {      case  None  =>  println("No  shoe  recommendation")      case  Some(shoe)  =>  println(s"Shoe  recommendation:  $shoe")  }

Page 4: Unsucking Error Handling with Futures

Future[A]Asynchronous Computation

Page 5: Unsucking Error Handling with Futures

def  userForEmail(email:  String):  Future[User]  =  ???  def  shoeSizeForUser(user:  User):  Future[Size]  =  ???  def  recommendedShoeStyleForSize(size:  Size):  Future[ShoeStyle]    =  ???  

def  recommendedShoe(email:  String):  Future[ShoeStyle]  =  for  {      user  <-­‐  userForEmail(email)      size  <-­‐  shoeSizeForUser(user)      shoe  <-­‐  recommendedShoeStyleForSize(size)  }  yield  shoe

recommendedShoe("[email protected]")  onComplete  {      case  Failure(t)  =>  println("Shoe  recommendation  failed")      case  Success(shoe)  =>  println(s"Shoe  recommendation:  $shoe")  }

Page 6: Unsucking Error Handling with Futures

Future[Option[A]]Errors (possible missing values)

+ asynchronous computation

Page 7: Unsucking Error Handling with Futures

def  userForEmail(email:  String):  Future[Option[User]]  =  ???  

def  shoeSizeForUser(userOpt:  Option[User]):  Future[Option[Size]]  =  {      userOpt  match  {          case  None  =>  Future.successful(None)          case  Some(user)  =>  ???      }  }  

def  recommendedShoeStyleForSize(sizeOpt:  Option[Size]):  Future[Option[ShoeStyle]]  =  {      sizeOpt  match  {          case  None  =>  Future.successful(None)          case  Some(size)  =>  ???      }  }  

def  recommendedShoe(email:  String):  Future[Option[ShoeStyle]]  =  for  {      user  <-­‐  userForEmail(email)      size  <-­‐  shoeSizeForUser(user)      shoe  <-­‐  recommendedShoeStyleForSize(size)  }  yield  shoe

recommendedShoe("[email protected]")  onComplete  {      case  Failure(t)  =>  println("Shoe  recommendation  failed")      case  Success(None)  =>  println("No  shoe  recommendation")      case  Success(Some(shoe))  =>  println(s"Shoe  recommendation:  $shoe")  }

Page 8: Unsucking Error Handling with Futures

def  userForEmail(email:  String):  Future[Option[User]]  =  ???  

def  shoeSizeForUser(userOpt:  Option[User]):  Future[Option[Size]]  =  {      userOpt  match  {          case  None  =>  Future.successful(None)          case  Some(user)  =>  ???      }  }  

def  recommendedShoeStyleForSize(sizeOpt:  Option[Size]):  Future[Option[ShoeStyle]]  =  {      sizeOpt  match  {          case  None  =>  Future.successful(None)          case  Some(size)  =>  ???      }  }  

def  recommendedShoe(email:  String):  Future[Option[ShoeStyle]]  =  for  {      user  <-­‐  userForEmail(email)      size  <-­‐  shoeSizeForUser(user)      shoe  <-­‐  recommendedShoeStyleForSize(size)  }  yield  shoe

recommendedShoe("[email protected]")  onComplete  {      case  Failure(t)  =>  println("Shoe  recommendation  failed")      case  Success(None)  =>  println("No  shoe  recommendation")      case  Success(Some(shoe))  =>  println(s"Shoe  recommendation:  $shoe")  }

Page 9: Unsucking Error Handling with Futures

–Dave Thomas

“Every piece of knowledge must have a single, unambiguous, authoritative representation

within a system.”

Page 10: Unsucking Error Handling with Futures

def  flatMap[B](f:  A  =>  F[B]):  F[B]  Given  an  F[A]:

Page 11: Unsucking Error Handling with Futures

def  flatMap[B](f:  A  =>  F[B]):  F[B]  Given  an  F[A]:

case  class  FutureOption[+A](future:  Future[Option[A]])  {      def  flatMap[B](f:  A  =>  FutureOption[B]):  FutureOption[B]  =  {          val  result  =  future  flatMap  {              case  None  =>  Future.successful(None)              case  Some(opt)  =>  f(opt).future          }  

       FutureOption(result)      }  }

Page 12: Unsucking Error Handling with Futures

def  userForEmail(email:  String):  Future[Option[User]]  =  ???  def  shoeSizeForUser(user:  User):  Future[Option[Size]]  =  ???  def  recommendedShoeStyleForSize(size:  Size):  Future[Option[ShoeStyle]]  =  ???  

def  recommendedShoe(email:  String):  FutureOption[ShoeStyle]  =  for  {      user  <-­‐  FutureOption(userForEmail(email))      size  <-­‐  FutureOption(shoeSizeForUser(user))      shoe  <-­‐  FutureOption(recommendedShoeStyleForSize(size))  }  yield  shoe

recommendedShoe("[email protected]").future  onComplete  {      case  Failure(t)  =>  println("Shoe  recommendation  failed")      case  Success(None)  =>  println("No  shoe  recommendation")      case  Success(Some(shoe))  =>  println(s"Shoe  recommendation:  $shoe")  }

Page 13: Unsucking Error Handling with Futures

def  userForEmail(email:  String):  Future[Option[User]]  =  ???  def  shoeSizeForUser(user:  User):  Future[Option[Size]]  =  ???  def  recommendedShoeStyleForSize(size:  Size):  Future[Option[ShoeStyle]]  =  ???  

def  recommendedShoe(email:  String):  FutureOption[ShoeStyle]  =  for  {      user  <-­‐  FutureOption(userForEmail(email))      size  <-­‐  FutureOption(shoeSizeForUser(user))      shoe  <-­‐  FutureOption(recommendedShoeStyleForSize(size))  }  yield  shoe

recommendedShoe("[email protected]").future  onComplete  {      case  Failure(t)  =>  println("Shoe  recommendation  failed")      case  Success(None)  =>  println("No  shoe  recommendation")      case  Success(Some(shoe))  =>  println(s"Shoe  recommendation:  $shoe")  }

Page 14: Unsucking Error Handling with Futures

def  userForEmail(email:  String):  Future[Option[User]]  =  ???  def  shoeSizeForUser(user:  User):  Future[Option[Size]]  =  ???  def  recommendedShoeStyleForSize(size:  Size):  Future[Option[ShoeStyle]]  =  ???  

def  recommendedShoe(email:  String):  FutureOption[ShoeStyle]  =  for  {      user  <-­‐  FutureOption(userForEmail(email))      size  <-­‐  FutureOption(shoeSizeForUser(user))      shoe  <-­‐  FutureOption(recommendedShoeStyleForSize(size))  }  yield  shoe

recommendedShoe("[email protected]").future  onComplete  {      case  Failure(t)  =>  println("Shoe  recommendation  failed")      case  Success(None)  =>  println("No  shoe  recommendation")      case  Success(Some(shoe))  =>  println(s"Shoe  recommendation:  $shoe")  }

Page 15: Unsucking Error Handling with Futures

N*N types =

N2 Implementations?

Page 16: Unsucking Error Handling with Futures

Monad Transformers To the Rescue

Scalaz

cats

Page 17: Unsucking Error Handling with Futures

OptionT[F[_], A]

Wraps F[Option[A]]

Works for any F[_] (as long as F[_] is a monad)

Implements passing None through the F[_] effect

e.g. OptionT[Future, A] is a wrapper for Future[Option[A]]

Page 18: Unsucking Error Handling with Futures

import  scala.concurrent.Future  import  scala.concurrent.ExecutionContext.Implicits.global import  scalaz._  import  Scalaz._  

def  userForEmail(email:  String):  Future[Option[User]]  =  ???  def  shoeSizeForUser(user:  User):  Future[Option[Size]]  =  ???  def  recommendedShoeStyleForSize(size:  Size):  Future[Option[ShoeStyle]]  =  ???  

def  recommendedShoe(email:  String):  OptionT[Future,  ShoeStyle]  =  for  {      user  <-­‐  OptionT(userForEmail(email))      size  <-­‐  OptionT(shoeSizeForUser(user))      shoe  <-­‐  OptionT(recommendedShoeStyleForSize(size))  }  yield  shoe

recommendedShoe("[email protected]").run  onComplete  {      case  Failure(t)  =>  println("Shoe  recommendation  failed")      case  Success(None)  =>  println("No  shoe  recommendation")      case  Success(Some(shoe))  =>  println(s"Shoe  recommendation:  $shoe")  }

libraryDependencies += "org.scalaz" %% "scalaz-core" % "7.1.4"

Page 19: Unsucking Error Handling with Futures

Either[A, B]Errors with a “reason”

Page 20: Unsucking Error Handling with Futures

Either[A, B]: not nearly as useful as it could be.

In scalaz, use: A \/ B — same as \/[A, B]

In cats, use: A Xor B — same as Xor[A, B]

Page 21: Unsucking Error Handling with Futures

def  userForEmail(email:  String):  Future[String  \/  User]  =  ???  def  shoeSizeForUser(userOpt:  String  \/  User):  Future[String  \/  Size]  =  {      userOpt.fold(          err  =>  Future.successful(err.left),          user  =>  ???      )  }  

def  recommendedShoeStyleForSize(sizeOpt:  String  \/  Size):  Future[String  \/  ShoeStyle]  =  {      sizeOpt.fold(          err  =>  Future.successful(err.left),          size  =>  ???      )  }  

def  recommendedShoe(email:  String):  Future[String  \/  ShoeStyle]  =  for  {      user  <-­‐  userForEmail(email)      size  <-­‐  shoeSizeForUser(user)      shoe  <-­‐  recommendedShoeStyleForSize(size)  }  yield  shoe

recommendedShoe("[email protected]")  onComplete  {      case  scala.util.Failure(t)  =>  println("Shoe  recommendation  failed")      case  scala.util.Success(res)  =>          res.fold(              err  =>  println(s"No  shoe  recommendation,  reason:  $err"),              shoe  =>  println(s"Shoe  recommendation:  $shoe")          )  }

Page 22: Unsucking Error Handling with Futures

def  userForEmail(email:  String):  Future[String  \/  User]  =  ???  def  shoeSizeForUser(userOpt:  String  \/  User):  Future[String  \/  Size]  =  {      userOpt.fold(          err  =>  Future.successful(err.left),          user  =>  ???      )  }  

def  recommendedShoeStyleForSize(sizeOpt:  String  \/  Size):  Future[String  \/  ShoeStyle]  =  {      sizeOpt.fold(          err  =>  Future.successful(err.left),          size  =>  ???      )  }  

def  recommendedShoe(email:  String):  Future[String  \/  ShoeStyle]  =  for  {      user  <-­‐  userForEmail(email)      size  <-­‐  shoeSizeForUser(user)      shoe  <-­‐  recommendedShoeStyleForSize(size)  }  yield  shoe

recommendedShoe("[email protected]")  onComplete  {      case  scala.util.Failure(t)  =>  println("Shoe  recommendation  failed")      case  scala.util.Success(res)  =>          res.fold(              err  =>  println(s"No  shoe  recommendation,  reason:  $err"),              shoe  =>  println(s"Shoe  recommendation:  $shoe")          )  }

Page 23: Unsucking Error Handling with Futures

def  userForEmail(email:  String):  Future[String  \/  User]  =  ???  def  shoeSizeForUser(user:  User):  Future[String  \/  Size]  =  ???  def  recommendedShoeStyleForSize(size:  Size):  Future[String  \/  ShoeStyle]  =  ???  

def  recommendedShoe(email:  String):  EitherT[Future,  String,  ShoeStyle]  =  for  {      user  <-­‐  EitherT(userForEmail(email))      size  <-­‐  EitherT(shoeSizeForUser(user))      shoe  <-­‐  EitherT(recommendedShoeStyleForSize(size))  }  yield  shoe

recommendedShoe("[email protected]").run  onComplete  {      case  scala.util.Failure(t)  =>  println("Shoe  recommendation  failed")      case  scala.util.Success(res)  =>          res.fold(              err  =>  println(s"No  shoe  recommendation,  reason:  $err"),              shoe  =>  println(s"Shoe  recommendation:  $shoe")          )  }

Page 24: Unsucking Error Handling with Futures

Either vs Option

Page 25: Unsucking Error Handling with Futures

Some(3) \/> "no value" == 3.rightNone \/> "no value" == "no value".left

Converting Option to \/ (Either)

Page 26: Unsucking Error Handling with Futures

Representing Errors in Play Framework

Page 27: Unsucking Error Handling with Futures

type  Response[A]  =  EitherT[Future,  Result,  A]

Page 28: Unsucking Error Handling with Futures

type  Response[A]  =  EitherT[Future,  Result,  A]

Response[String] EitherT[Future,  Result,  String]

Page 29: Unsucking Error Handling with Futures

type  Response[A]  =  EitherT[Future,  Result,  A]

Response[String] EitherT[Future,  Result,  String]

Response[User] EitherT[Future,  Result,  User]

Page 30: Unsucking Error Handling with Futures

type  Response[A]  =  EitherT[Future,  Result,  A]

Response[String] EitherT[Future,  Result,  String]

Response[User]

Response[UserAndAuthInfo]

EitherT[Future,  Result,  User]

EitherT[Future,  Result,  UserAndAuthInfo]

Page 31: Unsucking Error Handling with Futures

type  Response[A]  =  EitherT[Future,  Result,  A]

Response[String] EitherT[Future,  Result,  String]

Response[User]

Response[UserAndAuthInfo]

Response[Result]

EitherT[Future,  Result,  User]

EitherT[Future,  Result,  UserAndAuthInfo]

EitherT[Future,  Result,  Result]

Page 32: Unsucking Error Handling with Futures

EitherT[Future,  Result,  Result]

def  merge(implicit  ev:  A  =:=  B)  =  fold(left  =>  ev(left),  right  =>  right)

Future[Result]

Page 33: Unsucking Error Handling with Futures

def  response(block:  =>  Response[Result]):  Action[AnyContent]  =      Action.async(block.merge)  

def  response[A](bodyParser:  BodyParser[A])(block:  Request[A]  =>  Response[Result]):  Action[A]  =      Action.async(bodyParser)(req  =>  block(req).merge)  

def  response(block:  Request[AnyContent]  =>  Response[Result]):  Action[AnyContent]  =      response(BodyParsers.parse.default)(block)  

Page 34: Unsucking Error Handling with Futures

type  Response[A]  =  EitherT[Future,  Result,  A]  

object  Response  {    def  fromFuture[A](o:  Future[A]):  Response[A]  =          EitherT(o.map(_.right))  

   def  fromFutureOption[A](noValue:  =>  Result)(o:  Future[Option[A]]):  Response[A]  =          EitherT(o.map(_  \/>  noValue))  

   def  fromFutureEither[A,  B](err:  A  =>  Result)(o:  Future[A  \/  B]):  Response[B]  =          EitherT(o.map(_.leftMap(err)))  

   def  fromOption[A](noValue:  =>  Result)(o:  Option[A]):  Response[A]  =          EitherT(Future.successful(o  \/>  noValue))  

   def  fromEither[A,  B](e:  A  =>  Result)(o:  A  \/  B):  Response[B]  =          EitherT(Future.successful(o.leftMap(e)))  

   def  fromTry[A](t:  Throwable  =>  Result)(o:  Try[A]):  Response[A]  =  {          val  eitherResult  =  o  match  {              case  scala.util.Success(s)  =>  s.right              case  scala.util.Failure(f)  =>  t(f).left          }  

       EitherT(Future.successful(eitherResult))      }  }

Page 35: Unsucking Error Handling with Futures

def  getUserByEmail(email:  String):  Future[String  \/  User]  =  ???  def  getFavouriteProducts(user:  User):  Future[String  \/  Seq[String]]  =  ???

 def  productsByEmail(email:  String)  =  response  {          for  {              user          <-­‐  getUserByEmail(email)            |>  fromFutureEither(s  =>  InternalServerError(s))              products  <-­‐  getFavouriteProducts(user)  |>  fromFuture          }  yield  Ok(views.html.productsByEmail(user,  products))      }

type  Response[A]  =  EitherT[Future,  Result,  A]  

object  Response  {    def  fromFuture[A](o:  Future[A]):  Response[A]  =          EitherT(o.map(_.right))  

 def  fromFutureEither[A,  B](err:  A  =>  Result)(o:  Future[A  \/  B]):  Response[B]  =          EitherT(o.map(_.leftMap(err)))  }

f(a) == a |> f

Page 36: Unsucking Error Handling with Futures

Domain-specific Errors

Page 37: Unsucking Error Handling with Futures

sealed  trait  UserServiceError  

private[controllers]  case  class  AuthError(why:  String)  extends  UserServiceError  private[controllers]  case  object  ConnectionError  extends  UserServiceError  

object  UserServiceError  {      def  authError(why:  String):  UserServiceError  =  AuthError(why)      val  connectionError:  UserServiceError  =  ConnectionError  

   def  fold[A](authError:  String  =>  A,  connectionError:  =>  A)(u:  UserServiceError)  =  {          u  match  {              case  AuthError(why)  =>  authError(why)              case  ConnectionError  =>  connectionError          }      }  }

Page 38: Unsucking Error Handling with Futures

sealed  trait  UserServiceError  

private[controllers]  case  class  AuthError(why:  String)  extends  UserServiceError  private[controllers]  case  object  ConnectionError  extends  UserServiceError

def  getUserByEmail(email:  String):  Future[UserServiceError  \/  User]  =  ???  def  getFavouriteProducts(user:  User):  Future[String  \/  Seq[String]]  =  ???

   val  userServiceErrorToResult  =  UserServiceError.fold(          authError  =  Forbidden(_),          connectionError  =  InternalServerError("foobar")      )  _  

   def  productsByEmail(email:  String)  =  response  {          for  {              user          <-­‐  getUserByEmail(email)            |>  fromFutureEither(userServiceErrorToResult)              products  <-­‐  getFavouriteProducts(user)  |>  fromFuture          }  yield  Ok(views.html.productsByEmail(user,  products))      }  

Page 39: Unsucking Error Handling with Futures

Questions?