An error handling library based on Shapeless Coproducts, with a focus on type-safety, readability and ergonomic!

Release (https://badges.gitter.im/gitterHQ/gitter.png)

Installation

libraryDependencies += "com.github.jatcwang" %% "hotpotato-core" % LATEST_VERSION

Quick Example

In the example below, we’re trying to find an item and then buy it (providing a user ID).

It demonstrates some of the features of this library, such as:

  • Handling errors partially (mapErrorSome)
  • Handling errors exhaustively (mapErrorAllInto)
  • Dying on certain errors (i.e. terminating the whole execution chain)
def findItem(id: String): IO[OneOf2[ItemNotFound, ItemOutOfStock], Item] = 
  IO.fromEither {
    if (id == "itm1") Right(Item(id = "itm1", name = "Teddy Bear"))
    else Left(ItemNotFound().embedInto[OneOf2[ItemNotFound, ItemOutOfStock]])
  }


def buyItem(item: Item, userId: UserId): IO[OneOf3[FraudDetected, InsufficientFunds, NotAuthorized], BoughtItem] = {
  implicit val embedder: Embedder[OneOf3[FraudDetected, InsufficientFunds, NotAuthorized]] =
    Embedder.make
  IO.fromEither {
    if (userId == "frauduser") {
      Left(FraudDetected().embed)
    } else if (userId == "pooruser") {
      Left(InsufficientFunds().embed)
    } else {
      Right(BoughtItem(item))
    }
  }
}


def findAndBuy(
  itemId: ItemId,
  userId: UserId,
): IO[Nothing, String] = {
  implicit val embedder: Embedder[OneOf3[ItemNotFound, ItemOutOfStock, PurchaseDenied]] =
    Embedder.make
  (for {
    item <- findItem(itemId).embedError
    boughtItem <- buyItem(item, userId)

                   // Partial handling: converting some errors to another error
                   .mapErrorSome(
                     (e: InsufficientFunds) => PurchaseDenied("You don't have enough funds", e),
                     (e: NotAuthorized) =>
                       PurchaseDenied("You're not authorized to make purchases", e),
                   )
                   
                   // Terminate the whole computation if we encounter something fatal
                   .dieIf[FraudDetected]
                   .embedError
  } yield boughtItem)
    .flatMap(boughtItem => IO.succeed(s"Bought ${boughtItem.item.name}!"))

    // Exhaustive error handling, turning all errors into a user-friendly message
    .flatMapErrorAllInto[Nothing](
      (_: ItemNotFound)   => IO.succeed("item not found!"),
      (_: ItemOutOfStock) => IO.succeed("item out of stock"),
      (e: PurchaseDenied) => IO.succeed(s"Cannot purchase item because: ${e.msg}"),
    )
}

zioRuntime.unsafeRunSync(findAndBuy(itemId = "invalid_itm_id", userId = "user1"))
// res0: Exit[Nothing, String] = Success("item not found!")
zioRuntime.unsafeRunSync(findAndBuy(itemId = "itm1", userId           = "pooruser"))
// res1: Exit[Nothing, String] = Success(
//   "Cannot purchase item because: You don't have enough funds"
// )
zioRuntime.unsafeRunSync(findAndBuy(itemId = "itm1", userId           = "frauduser")).toEither
// res2: Either[Throwable, String] = Left(
//   FiberFailure(
//     Traced(
//       Die(repl.Session$App$FraudDetected),
//       ZTrace(Id(1576789551482L, 2L), List(), List(), None)
//     )
//   )
// )
zioRuntime.unsafeRunSync(findAndBuy(itemId = "itm1", userId           = "gooduser"))
// res3: Exit[Nothing, String] = Success("Bought Teddy Bear!")