Opening
When we think about failure in software, we tend to think in binary terms. It either worked or it did not. The API call succeeded or it failed. The database write went through or it threw an exception. But in the real world, failure is almost never binary. It is partial, messy, and ambiguous. Your API call succeeded but returned incomplete data. Your database write went through but the network sync that was supposed to follow it failed. The network works for reads but not for writes. Partial failure is the normal state of distributed systems. Full success and full failure are actually the rare exceptions. And once you understand this, you will design your apps very differently.
What Partial Failure Looks Like
Let me give you concrete examples from real Android development. You make an API call to load a user profile. The call returns HTTP 200 — success. But the avatar URL is null because the image service is down. The profile loaded, but partially. Your app saves a new post to the local database and then tries to sync it to the server. The database write succeeds. The network sync fails because the user walked into a subway. The post exists locally but not on the server. You are in a partial state. You fetch a list of products with their prices and availability. The product service responds, but the pricing service timed out. You get products without prices. Partial data. In each of these cases, the operation did not fully succeed and did not fully fail. It is somewhere in between. And your code needs to handle this middle ground.
Why Binary Error Handling Breaks
Most error handling in Android apps is binary. Try-catch around the whole operation. If anything throws, show an error screen. If nothing throws, show the success screen. This breaks with partial failures. If your profile API returns the user's name and bio but not their avatar, do you show an error screen? That would be wrong — you have most of the data. Do you show the success screen? Also wrong — the avatar is missing. The right answer is somewhere between: show what you have, indicate what is missing, maybe retry the missing part. Binary try-catch cannot express this. You need a more nuanced model that can represent "mostly succeeded with these specific failures."
Modeling Partial States with Sealed Classes
Kotlin's sealed classes and sealed interfaces are perfect for modeling partial failure. Instead of just Success and Error, you can model the full spectrum. Consider a sealed interface called ProfileState. You have Loading. You have a FullSuccess with all the data. You have PartialSuccess with the user data but a list of what is missing — maybe the avatar, maybe the follower count. You have an Error for complete failure. Your UI can now react intelligently. FullSuccess: show everything. PartialSuccess: show what you have, display a subtle indicator for missing data, offer a retry button for just the failed parts. Error: show the full error screen. This is more work than binary handling, but it matches reality. And matching reality is what good software does.
The Result Type for Individual Operations
At a finer granularity, Kotlin's Result type — or a custom sealed class — lets you model the outcome of each individual operation. Say you need to load a user profile, their posts, and their followers. Three operations. Each can succeed or fail independently. You launch all three concurrently and collect their results. Maybe the profile succeeds, posts succeed, but followers fail because that service is down. You now have Result.success for profile, Result.success for posts, and Result.failure for followers. Your UI shows the profile and posts immediately, and displays "couldn't load followers, tap to retry" where the follower section would be. This is graceful degradation. The user gets most of what they need instead of staring at a full error screen because one out of three operations failed.
Error Boundaries in Compose
Jetpack Compose does not have a built-in error boundary like React does, but you can build the concept yourself. An error boundary is a wrapper Composable that catches failures in a child Composable and renders a fallback instead of crashing the entire screen. You can implement this with a try-catch around a Composable invocation using a remember block and a state-based approach. If the user data loaded but the avatar failed to decode, the avatar Composable shows a placeholder instead of crashing the whole profile screen. The key insight is: scope your error handling to the component that failed, not to the entire screen. If one section fails, degrade that section. Do not take down the whole page. This requires thinking about each component as independently fallible, which is a different mindset from wrapping everything in one big try-catch.
The Offline-First Pattern
The most common form of partial failure on mobile is network failure. The user loses connectivity but the app still needs to function. The offline-first pattern is designed exactly for this. Your app writes to the local database first — always. Then it tries to sync with the server. If the sync succeeds, great. If it fails, the local state is still consistent, and the app queues the sync for later. The user never sees an error for the write — they see their data saved. The sync happens when connectivity returns. This is a form of embracing partial failure: accepting that the local and remote states will sometimes be inconsistent, and designing your system to eventually reconcile them. Room with a sync queue is the most common Android implementation.
Check Each Step, Handle Each Failure
Here is a practical pattern I want you to adopt. For any multi-step operation, check each step independently. Do not wrap the entire sequence in one try-catch. Instead, execute each step, check its result, and decide how to proceed. Step one: fetch user from API. If that fails, show cached data if available. Step two: fetch user's posts. If that fails, show the profile without posts and offer a retry. Step three: update the last-seen timestamp on the server. If that fails, silently ignore it — it is non-critical. Each failure has a different severity and a different response. Treating them uniformly — all-or-nothing — is lazy error handling that punishes the user for problems they should not even notice.
Kotlin Patterns for Partial Failure
Let me show you practical Kotlin patterns. Use runCatching to wrap individual operations and get a Result. Use mapCatching and recover to transform and handle failures in a chain. Use coroutineScope with async to launch parallel operations and collect individual results. A powerful pattern is a data class that holds the results of multiple operations: data class ProfileScreen with val user of type Result of User, val posts of type Result of List of Post, val followers of type Result of Int. Each field can independently succeed or fail. Your UI maps over this: user dot getOrNull let render profile, posts dot getOrNull let render posts, followers dot onFailure show retry. This pattern is explicit about which parts can fail and how the UI should respond to each.
The Network That Half Works
Let me describe a particularly tricky partial failure scenario. Your network works for GET requests but not for POST requests. This happens more often than you think — corporate firewalls, proxy servers, or ISP-level filtering can block certain HTTP methods while allowing others. Your app can read data but not write it. If your error handling is binary — network works or it does not — you cannot represent this state. But with per-operation error handling, it is natural. Reads succeed. Writes fail. The user can browse content but cannot submit forms. Your app should communicate this clearly: "You can view content, but saving is temporarily unavailable." This requires thinking about each network operation as independently fallible, which is the theme of this entire video.
Designing for Partial Failure
Let me give you a design checklist for partial failure resilience. First: identify all the operations in your user flow and list which ones can fail independently. Second: for each operation, decide the fallback — cached data, placeholder UI, graceful omission, or retry option. Third: model your state to represent partial success — sealed classes with rich variants, not just Loading, Success, Error. Fourth: scope your error handling to the smallest possible component. Fifth: always give the user a way to retry the specific thing that failed, not just a "reload everything" button. Sixth: log partial failures even when the user does not see them — you need this data to understand your system's real reliability.
Closing
The mindset shift I want you to make is this: stop thinking about success and failure as binary. Think about them as a spectrum. Most operations result in partial outcomes — some things work, some do not. Your app's job is to give the user the best possible experience with whatever succeeded, and to gracefully handle whatever failed. This means richer state models, more granular error handling, and a UI that can render incomplete data. It is more work than showing a full error screen, but it is enormously better for users. In the next video, we are going to talk about timeouts and retries — because when things do fail, how long you wait and whether you try again makes all the difference.