Scala
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Scala
-
Section 4: Functional Scala
-
Section 5: Collections in Depth
-
Section 6: Type System
-
Section 7: Concurrency and Ecosystem
-
Section 8: Practical Projects
-
Section 9: Interview Practice
-
Section 10: Data Structures and Algorithms in Scala
-
Section 11: More Practice Exercises
-
Section 12: Advanced Functional Patterns
-
Section 13: More Ecosystem
-
Section 14: Scala Collections Library Deep Dive
-
Section 15: Scala Standard Library Deep Dive
-
Section 16: Akka Ecosystem Deep Dive
-
Section 17: Cats and Cats Effect Deep Dive
-
Section 18: Play Framework Deep Dive
-
Section 19: Apache Spark with Scala Deep Dive
-
Section 20: Scala Build Tools Deep Dive
-
Section 21: Scala 3 Specific Features
-
90: Union and Intersection Types
-
Section 22: Scala Testing Deep Dive
-
Section 23: Functional Domain Modeling
-
Section 24: More Data Structures and Algorithms in Scala
-
Section 25: Scala for Data Engineering
-
Section 26: More Practical Projects
-
Section 27: More Interview and Review
-
Section 28: ZIO Ecosystem Deep Dive
-
Section 29: Scala for Machine Learning
-
Section 30: Scala Microservices Architecture
-
Section 31: Scala Type System Deep Dive
-
Section 32: More Practice and Drills
-
Section 33: Scala Performance Deep Dive
-
Section 34: Scala Ecosystem Tooling
-
Section 35: Scala for Reactive Systems
-
Section 36: More Real-World Case Studies
-
Section 37: Scala for Financial Systems
-
Section 38: Scala GraphQL and gRPC
-
Section 39: More Final Projects
-
Section 40: More Interview and Final Review
-
Section 41: Scala for Streaming Data
-
Section 42: Scala Security Practices
-
Section 43: More Language Deep Dive
-
Section 44: Scala Command-Line Tools
-
Section 45: Scala Documentation and Style
-
Section 46: Scala Dependency Management
-
Section 47: More Practical Backend Patterns
-
Section 48: Scala for Event-Driven Architecture
-
Section 49: More Practice Drills Round 2
-
Section 50: Scala Compiler Deep Dive
-
Section 51: Scala for Web Frontends
-
Section 52: More Data Engineering Practice
-
Section 53: Scala Observability
-
Section 54: More Advanced Practice Projects
-
Section 55: Scala for Legacy Java Integration
-
Section 56: More Testing Practice
-
Section 57: Final Mastery Review
-
Section 58: Scala History and Ecosystem Context
-
Section 59: More Concurrency Patterns
-
Section 60: Scala for Configuration Management
-
Section 61: More Domain Modeling Practice
122: Error Handling in ZIO
When I first started moving from standard Scala to ZIO, I had a habit I couldn't shake: I kept wrapping everything in try-catch blocks. I figured that since ZIO effects are just descriptions of programs, I should protect those descriptions with the same tools I used for imperative code. If a database call might throw an SQLException, I'd just wrap the call in a try block inside a ZIO.attempt. It felt safe. It felt familiar.
The Try-Catch Safety Net Illusion
Here is the problem: when you use a native try-catch inside a ZIO effect, you are essentially hiding the error from the ZIO runtime and the type system. Look at this snippet:
def fetchUser(id: String): ZIO[Any, Nothing, User] = {
ZIO.attempt {
try {
repository.findUser(id)
} catch {
case e: Exception =>
println(s"Log: user $id not found")
User.Guest // Return a fallback value
}
}
}
At first glance, this looks fine. But look at the return type: ZIO[Any, Nothing, User]. I've told the compiler—and any other developer using this method—that this effect cannot fail (that's what Nothing in the error channel means). I've effectively lied to the type system. If the fallback User.Guest isn't actually appropriate for the business logic downstream, the calling code has no way to know that a failure occurred. We've traded type safety for a feeling of familiarity, and in a large system, that's how bugs sneak into production.
Embracing the Error Channel
In ZIO, we don't hide errors; we promote them. The E in ZIO[R, E, A] is your best friend. Instead of catching the error immediately to return a "safe" value, we lift the exception into the error channel and let the caller decide how to handle it. I prefer defining a sealed trait for domain errors so the compiler can help me ensure I've handled every possible failure case.
Let's rewrite that user fetch using a proper domain error:
sealed trait UserError
case class UserNotFound(id: String) extends UserError
case class DatabaseError(cause: Throwable) extends UserError
def fetchUser(id: String): ZIO[Any, UserError, User] = {
ZIO.attempt(repository.findUser(id))
.mapError {
case e: NoSuchElementException => UserNotFound(id)
case other => DatabaseError(other)
}
}
Now the signature is honest: ZIO[Any, UserError, User]. The person calling this function is now forced to deal with UserError. If they want to provide a fallback, they can use .catchAll or .orElse. This keeps the "happy path" clean while making the "sad path" explicit.
I want to highlight two key combinators you'll use constantly: catchAll and catchSome. Use catchAll when you have a general recovery strategy for any error of type E. Use catchSome when you only care about one specific failure—say, retrying on a Timeout but crashing on a PermissionDenied.
One last tip: avoid using ZIO.attempt for everything. If you are already inside the ZIO world and you want to trigger a failure intentionally based on a business condition, use ZIO.fail(error). It's cleaner, it's faster, and it doesn't involve the overhead of creating a JVM exception stack trace.
📋 Practical Task
Build a Resilient Payment Gateway Handler
You are integrating a third-party payment API. The API is flaky: it sometimes throws a TimeoutException, and occasionally it returns a PaymentDeclinedException. You need to implement a handler that follows these specific business rules:
- If a
TimeoutExceptionoccurs, the system should retry the operation exactly 3 times before finally failing with aGatewayTimeoutdomain error. - If a
PaymentDeclinedExceptionoccurs, it should not retry; it should immediately fail with aTransactionDenieddomain error. - Any other unexpected exception should be mapped to a general
InternalSystemError.
Requirements:
- Define a sealed trait
PaymentErrorwith the three cases mentioned above. - Create a function
processPayment(amount: Double): ZIO[Any, PaymentError, Receipt]. - Use
ZIO.attemptto wrap the flaky API call. - Use
.retryfor the timeout logic and.catchAllor.mapErrorto transform the exceptions into your domainPaymentErrortypes.
There are no comments for now.