An error handling library based on Shapeless Coproducts, with a focus on type-safety, readability and ergonomic!
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(1584095467171L, 2L),
// List(
// SourceLocation(
// "ErrorTrans.scala",
// "hotpotato.ZioErrorTransThrow",
// "extractAndThrow",
// 109
// ),
// SourceLocation("ZIO.scala", "zio.ZIO", "bimap", 213),
// SourceLocation("ZIO.scala", "zio.ZIO$", "fromEither", 2577),
// SourceLocation(
// "index.md",
// "repl.Session$App$$anonfun$buyItem$1",
// "apply",
// 60
// ),
// SourceLocation(
// "index.md",
// "repl.Session$App$$anonfun$findAndBuy$1",
// "apply",
// 77
// ),
// SourceLocation("ZIO.scala", "zio.ZIO", "bimap", 213),
// SourceLocation("ZIO.scala", "zio.ZIO$", "fromEither", 2577),
// SourceLocation(
// "index.md",
// "repl.Session$App$$anonfun$findItem$1",
// "apply",
// 53
// )
// ),
// List(
// SourceLocation("ZIO.scala", "zio.ZIO", "bimap", 213),
// SourceLocation(
// "index.md",
// "repl.Session$App$$anonfun$findAndBuy$2",
// "apply",
// 96
// ),
// SourceLocation(
// "ErrorTransSyntax.scala",
// "hotpotato.ErrorTransSyntax$ErrorTransCoproductOps3",
// "flatMapErrorAllInto",
// 165
// ...
zioRuntime.unsafeRunSync(findAndBuy(itemId = "itm1", userId = "gooduser"))
// res3: Exit[Nothing, String] = Success("Bought Teddy Bear!")