This document discusses error management in ZIO compared to Future. It begins with an overview of ZIO and Future effects before comparing how each handles errors. Key differences noted are that Future throws errors away on a side channel while ZIO composes errors. The document recommends best practices for error handling in ZIO like extending exceptions in sealed traits and avoiding reflexive logging. It concludes by discussing how ZIO enables next-generation debugging by tracking fibers and continuations during asynchronous execution.
1. 1
Error Management
ZIO vs Future
Dublin Scala Meetup, May 11th
John A. De Goes @jdegoes
Kai @kaidaxofficial
...with help of Pavel Shirshov @shirshovp
2. Agenda
A Tale of Two Effects
2
Next-Gen Debugging Conclusion
Managing Errors
4. Parallel
Future enables parallel
computations and
non-blocking gathering.
Error
Future has a built-in error
channel for
Throwable-based errors.
Eager
Future is not referentially
transparent; refactoring
may change behavior.
4
FUTURE
Async
Future enables
non-blocking code that
efficiently uses threads.
6. Procedural Functional
def program: Unit = {
println("What’s your name?")
val name = readLine()
println(s"Howdy. $name!")
return ()
}
val program = for {
_ <- putStrLn("What’s your name?")
name <- getStrLn
_ <- putStrLn(s"Howdy, $name")
} yield ()
16. Fail Domain errors, business errors,
transient errors, expected errors...
Expected Errors
Not Reflected in Types
DieSystem errors, fatal errors,
unanticipated errors, defects...
Unexpected Errors
Reflected in Types
16
ERROR DUALITY
17. 17
ERROR DUALITY
Not Reflected in TypesNot Reflected in Types
val failed: Future[Nothing] =
Future.failed(new Exception)
val died: Future[Nothing] =
Future(throw new Error)
FUTURE
18. 18
ERROR DUALITY
Not Reflected in TypesReflected in Types
val failed: IO[String, Nothing] =
ZIO.fail(“Uh oh!”)
val died: IO[Nothing, Nothing] =
ZIO.dieMessage(“Uh oh!”)
40. 40
1. DON’T TYPE UNEXPECTED ERRORS
ZIO.effect(httpClient.get(url)).refineOrDie {
case e : TemporarilyUnavailable => e
}.retry(RetryPolicy).orDie
: IO[TemporarilyUnavailable, Response]
41. 41
2. DO EXTEND EXCEPTION WITH SEALED TRAITS
sealed trait UserServiceError
extends Exception
case class InvalidUserId(id: ID)
extends UserServiceError
case class ExpiredAuth(id: ID)
extends UserServiceError
UserServiceError
InvalidUserId ExpiredAuth
42. 42
2. DO EXTEND EXCEPTION WITH SEALED TRAITS
userServiceError match {
case InvalidUserId(id) => ...
case ExpiredAuth(id) => ...
}
43. 43
2. DO EXTEND EXCEPTION WITH SEALED TRAITS
for {
service <- userAuth(token)
_ <- service.userProfile(userId)
body <- generateEmail(orderDetails)
receipt <- sendEmail(“Your Order Details”,
body, profile.email)
} yield receipt
ExpiredAuth
44. 44
2. DO EXTEND EXCEPTION WITH SEALED TRAITS
for {
service <- userAuth(token)
_ <- service.userProfile(userId)
body <- generateEmail(orderDetails)
receipt <- sendEmail(“Your Order Details”,
body, profile.email)
} yield receipt
InvalidUserId
45. 45
2. DO EXTEND EXCEPTION WITH SEALED TRAITS
for {
service <- userAuth(token)
_ <- service.userProfile(userId)
body <- generateEmail(orderDetails)
receipt <- sendEmail(“Your Order Details”,
body, profile.email)
} yield receipt
Nothing
46. 46
2. DO EXTEND EXCEPTION WITH SEALED TRAITS
for {
service <- userAuth(token)
_ <- service.userProfile(userId)
body <- generateEmail(orderDetails)
receipt <- sendEmail(“Your Order Details”,
body, profile.email)
} yield receipt
EmailDeliveryError
47. 47
2. DO EXTEND EXCEPTION WITH SEALED TRAITS
for {
service <- userAuth(token)
_ <- service.userProfile(userId)
body <- generateEmail(orderDetails)
receipt <- sendEmail(“Your Order Details”,
body, profile.email)
} yield receipt
IO[Exception, Receipt]
49. 49
4. DO GET TO KNOW UIO
type UIO[+A] = ZIO[Any, Nothing, A]
Cannot fail!
50. lazy val processed: UIO[Unit] =
processUpload(upload).either.flatMap {
case Left (_) => processed.delay(1.minute)
case Right(_) => ZIO.succeed(())
}
50
4. DO GET TO KNOW UIO
Fails with UploadError
51. lazy val processed: UIO[Unit] =
processUpload(upload).either.flatMap {
case Left (_) => processed.delay(1.minute)
case Right(_) => ZIO.succeed(())
}
51
4. DO GET TO KNOW UIO
Fails with Nothing
52. 52
4. DO GET TO KNOW UIO
lazy val processed: UIO[Unit] =
processUpload(upload).either.flatMap {
case Left (_) => processed.delay(1.minute)
case Right(_) => ZIO.succeed(())
}
Fails with Nothing
55. 55
def asyncDbCall(sql: SQL): Future[Result]
def selectHumans(): Future[Result] = ...asyncDbCall(...)...
def selectPets(): Future[Result] = ...asyncDbCall(...)...
FUTURE
Which function failed, selectHumans or selectPets?
PostgresException: Syntax error at or near 42
at example$.getConnection(example.scala:43)
at example$.$anonfun$asyncDbCall$1(example.scala:23)
at scala.concurrent.Future$.$anonfun$apply$1(Future.scala:658)
at scala.util.Success.$anonfun$map$1(Try.scala:255)
at scala.util.Success.map(Try.scala:213)
at scala.concurrent.Future.$anonfun$map$1(Future.scala:292)
at scala.concurrent.impl.Promise.liftedTree1$1(Promise.scala:33)
at scala.concurrent.impl.Promise.$anonfun$transform$1(Promise.scala:33)
at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:64)
at java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1402)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
56. 56
Which function failed, selectHumans or selectPets?
FUTURE
def asyncDbCall(sql: SQL): Future[Result]
def selectHumans(): Future[Result] = ...asyncDbCall(...)...
def selectPets(): Future[Result] = ...asyncDbCall(...)...
Only the last operation is mentioned
PostgresException: Syntax error at or near 42
at example$.getConnection(example.scala:43)
at example$.$anonfun$asyncDbCall$1(example.scala:23)
at scala.concurrent.Future$.$anonfun$apply$1(Future.scala:658)
at scala.util.Success.$anonfun$map$1(Try.scala:255)
at scala.util.Success.map(Try.scala:213)
at scala.concurrent.Future.$anonfun$map$1(Future.scala:292)
at scala.concurrent.impl.Promise.liftedTree1$1(Promise.scala:33)
at scala.concurrent.impl.Promise.$anonfun$transform$1(Promise.scala:33)
at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:64)
at java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1402)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
57. 57
Which function failed, selectHumans or selectPets?
FUTURE
Only the last operation is mentioned
There is NO way to know!!!
def asyncDbCall(sql: SQL): Future[Result]
def selectHumans(): Future[Result] = ...asyncDbCall(...)...
def selectPets(): Future[Result] = ...asyncDbCall(...)...
PostgresException: Syntax error at or near 42
at example$.getConnection(example.scala:43)
at example$.$anonfun$asyncDbCall$1(example.scala:23)
at scala.concurrent.Future$.$anonfun$apply$1(Future.scala:658)
at scala.util.Success.$anonfun$map$1(Try.scala:255)
at scala.util.Success.map(Try.scala:213)
at scala.concurrent.Future.$anonfun$map$1(Future.scala:292)
at scala.concurrent.impl.Promise.liftedTree1$1(Promise.scala:33)
at scala.concurrent.impl.Promise.$anonfun$transform$1(Promise.scala:33)
at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:64)
at java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1402)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
60. 60
def myQuery =
for {
_ <- UIO(println(“Querying!”))
res <- queryDatabase
} yield res
Fiber:0 ZIO Execution trace:
at myQuery(example.scala:4)
at myQuery(example.scala:3)
Fiber:0 was supposed to continue to:
a future continuation at myQuery(example.scala:5)
Failure!
61. 61
def myQuery =
UIO(println(“Querying!”))
.flatMap(_ =>
queryDatabase
.map(res => res))
Fiber:0 ZIO Execution trace:
at myQuery(example.scala:4)
at myQuery(example.scala:3)
Fiber:0 was supposed to continue to:
a future continuation at myQuery(example.scala:5)
62. 62
def myQuery =
UIO(println(“Querying!”))
.flatMap(_ =>
queryDatabase
.map(res => res))
Fiber:0 ZIO Execution trace:
at myQuery(example.scala:4)
at myQuery(example.scala:3)
Fiber:0 was supposed to continue to:
a future continuation at myQuery(example.scala:5)
The Past
63. 63
def myQuery =
UIO(println(“Querying!”))
.flatMap(_ =>
queryDatabase
.map(res => res))
Fiber:0 ZIO Execution trace:
at myQuery(example.scala:4)
at myQuery(example.scala:3)
Fiber:0 was supposed to continue to:
a future continuation at myQuery(example.scala:5)
The Past
The Future
64. 64
def asyncDbCall(sql: SQL): Task[Result]
val selectHumans: Task[Result] = ...asyncDbCall(...)...
val selectPets: Task[Result] = ...asyncDbCall(...)...
65. 65
def asyncDbCall(sql: SQL): Task[Result]
val selectHumans: Task[Result] = ...asyncDbCall(...)...
val selectPets: Task[Result] = ...asyncDbCall(...)...
Fiber:0 ZIO Execution trace:
at asyncDbCall(example.scala:22)
at selectHumans(example.scala:26)
Fiber:0 was supposed to continue to:
a future continuation at selectHumans(example.scala:27)
66. 66
def asyncDbCall(sql: SQL): Task[Result]
val selectHumans: Task[Result] = ...asyncDbCall(...)...
val selectPets: Task[Result] = ...asyncDbCall(...)...
Fiber:0 ZIO Execution trace:
at asyncDbCall(example.scala:22)
at selectHumans(example.scala:26)
Fiber:0 was supposed to continue to:
a future continuation at selectHumans(example.scala:27)
Gotcha!
67. 67
EXECUTION TRACES
def doWork(condition: Boolean) = {
if (condition) {
doSideWork()
}
doMainWork()
}
java.lang.Exception: Worker failed!
at example$.doMainWork(example.scala:54)
at example$.doWork(example.scala:50)
at example$$anon$1.run(example.scala:60)
No mention of doSideWork()
PROCEDURAL
68. 68
EXECUTION TRACES
def doWork(condition: Boolean) =
for {
_ <- IO.when(condition)(doSideWork)
_ <- doMainWork
} yield ()
Fiber:0 ZIO Execution trace:
at example$.doMainWork(example.scala:27)
at example$.doWork(example.scala:23)
at example$.doSideWork(example.scala:26)
The conditional was true!
72. 72
● Tracing is fast , impact is negligible for real apps
● > 50x faster than Future
● ...even on synthetic benchmarks!
● Impact can be limited by config
● Much lower than monad transformers [10x]
● Enabled by default, no Java agents, no ceremony
Disable if tracing is a hot spot
effect.untraced
MADE FOR
PRODUCTION