Making an application truly robust is not about preventing errors from occurring, but about design the responsiveness of the system when things don't go as planned. Software that handles its failures gracefully is infinitely more reliable and easier to debug than software that tries to ignore problems or, worse, exhibits erratic and unpredictable behavior.
In the modern ecosystem, especially with Kotlin and Android, we find very powerful tools to prevent abrupt data flow interruptions. From classic error handling to... functional paradigm with ResultThe goal is always the same: to ensure the end user doesn't see an unexpected closing screen and that the developer has the exact information about what crashed and why.
Fundamental Error Control Strategies
When faced with code that can fail, the first line of defense is usually blocks. try/catch/finallyThe golden rule here is to catch exceptions from the most specific to the most general; if we put a base exception at the beginning, the derived exceptions will never be handled. It is vital to use the block finally or the using statement to free up resources, such as database connections or files, ensuring that no processes are left hanging even if the program crashes.
Sometimes, it's best not to throw the exception in the first place. If we know a condition is likely, it's preferable to use a if statement to validate the state before executing the action. For example, checking if a connection is already closed prevents throwing an unnecessary InvalidOperationException. In .NET, there are methods like Int32.TryParse which avoid the costly performance burden of throwing an exception when the error is part of the normal flow.
The Functional Approach: runCatching and Result
For those looking for cleaner, less nested code, Kotlin offers the feature runCatching and the Result typeThis is a discriminated join that encapsulates success or failure, allowing us to treat errors as data. Instead of abruptly jumping between blocks, we can chaining operations using map and flatMapThis makes the control flow much more predictable and elegant.
The true value of this pattern is the error compositionWe can defer fault handling until we reach the presentation layer (such as the ViewModel), preventing the business logic from being cluttered with repetitive try-catch blocks. However, we must be cautious and do not mix this functional approach with traditional exception handling in a single module to avoid confusion within the team.
Coroutines and Asynchronicity Management in Android
In the Android world, coroutines simplify asynchronous code, but introduce challenges in error handling. Understanding them is crucial. DispatchersDispatchers.Main for the UI, Dispatchers.IO for network or disk, and Dispatchers.Default for CPU-intensive calculations. The use of withContext(Dispatchers.IO) It ensures that the functions are safe for the main thread, preventing the app from freezing.
When we launch tasks, the difference between launch and async This is crucial. While `launch` is for "fire and forget" tasks, `async` returns a result using `await()`. The problem is that async can silence exceptions If `await()` is not called, it hides errors in the logs. To fix this globally, the CoroutineExceptionHandler It allows capturing unhandled errors in a specific scope, although for granular control, try-catch in the ViewModel remains a very reliable option.
Exception Architecture: Business vs. Program
Not all mistakes are the same. business exceptions These are the ones the user can understand and correct, such as an "insufficient balance." These should be descriptive and help the user solve the problem. On the other hand, program exceptions These are technical errors (such as a NullPointerException) that must be logged for the developer, but a generic message should be shown to the user to avoid leaking sensitive system data.
When designing custom exception classes, it is recommended inherit from RuntimeException To avoid the rigidity of Java's checked exceptions, which often force programmers to write empty catch blocks just to get the code to compile, a good practice is implement three basic constructors: one without parameters, another with a message, and a third that includes the original cause to maintain the stack trace.
Code Security and Quality Principles
From OWASP's perspective, error handling is a matter of security. It should never be show the stack trace to the end userThis is because it reveals the application's internal structure and facilitates attacks. Messages must be localized and grammatically correct, avoiding cryptic codes that add no value. Furthermore, it is imperative that the security logic... deny access by default if an exception occurs during permission validation.
To maintain data integrity, if a method performs multiple operations (such as a bank transfer) and one of them fails, it must be restore the original stateThis is achieved by catching the exception and performing a rollback action before re-throwing the original exception. ExceptionDispatchInfo so as not to lose the information of where the failure initially occurred. Share the information so that more users know about the topic.