error-handling

3 posts

line

Code Quality Improvement Techniques Part 23 (opens in new tab)

While early returns are a popular technique for clarifying code by handling error cases first, they should not be applied indiscriminately. This blog post argues that when error cases and normal cases share the same logic, integrating them into a single flow is often superior to branching. By treating edge cases as part of the standard execution path, developers can simplify their code and reduce unnecessary complexity. ### Unifying Edge Cases with Normal Logic Rather than treating every special condition as an error to be excluded via an early return, it is often more effective to design logic that naturally accommodates these cases. * For functions processing lists, standard collection operations like `map` or `filter` already handle empty collections without requiring explicit checks. * Integrating edge cases can lead to more concise code, though developers should be mindful of minor performance trade-offs, such as the overhead of creating sequence or list instances for empty inputs. * Unification ensures that the "main purpose" of the function remains the focus, rather than a series of guard clauses. ### Utilizing Language-Specific Safety Features Modern programming languages provide built-in operators and functions that allow developers to handle potential errors as part of the standard expression flow. * **Safe Navigation:** Use safe call operators (e.g., `?.`) and null-coalescing operators (e.g., `?:`) to handle null values as normal data flow rather than branching with `if (value == null)`. * **Collection Access:** Instead of manually checking if an index is within bounds, use functions like `getOrNull` or `getOrElse` to retrieve values safely. * **Property Dependencies:** In UI logic, instead of early returning when a string is empty, you can directly assign visibility and text values based on the condition (e.g., `isVisible = text.isNotEmpty()`). ### Functional Exception Handling When a process involves multiple steps that might throw exceptions, traditional early returns can lead to repetitive try-catch blocks and fragmented logic. * By using the `flatMap` pattern and Result-style types, developers can chain operations together. * Converting exceptions into specific error types within a wrapper (like a `Success` or `Error` sealed class) allows the entire sequence to be treated as a unified data flow. * This approach makes the overall business logic much clearer, as the "happy path" is represented by a clean chain of function calls rather than a series of nested or sequential error checks. Before implementing an early return, evaluate whether the edge case can be gracefully integrated into the main logic flow. If the language features or standard libraries allow the normal processing path to handle the edge case naturally, choosing integration over exclusion will result in more maintainable and readable code.

line

Code Quality Improvement Techniques Part (opens in new tab)

When implementing resource management patterns similar to Kotlin's `use` or Java's try-with-resources, developers often face the challenge of handling exceptions that occur during both primary execution and resource cleanup. Simply wrapping these multiple failures in a custom exception container can inadvertently break the calling code's error-handling logic by masking the original exception type. To maintain code quality, developers should prioritize the primary execution exception and utilize the `addSuppressed` mechanism to preserve secondary errors without disrupting the expected flow. ### The Risks of Custom Exception Wrapping Creating a new exception class to consolidate multiple errors during resource management can lead to significant issues for the caller. * Wrapping an expected exception, such as an `IOException`, inside a custom `DisposableException` prevents specific `catch` blocks from identifying and handling the original error. * This pattern often results in unhandled exceptions or the loss of specific error context, especially when the wrapper is hidden inside utility functions. * While this approach aims to be "neat" by capturing all possible failures, it forces the caller to understand the internal wrapping logic of the utility rather than the business logic errors. ### Prioritizing Primary Logic over Cleanup When errors occur in both the main execution block and the cleanup (e.g., `dispose()` or `close()`), it is critical to determine which exception takes precedence. * The exception from the main execution block is typically the "primary" failure that reflects a business logic or IO error, whereas a cleanup failure is often secondary. * Throwing a cleanup exception while discarding the primary error makes debugging difficult, as the root cause of the initial failure is lost. * In a typical `try-finally` block, if the `finally` block throws an exception, it naturally suppresses any exception thrown in the `try` block unless handled manually. ### Implementing Better Suppression Logic A more robust implementation mimics the behavior of Kotlin’s `Closeable.use` by ensuring the most relevant error is thrown while keeping others accessible for debugging. * Instead of creating a wrapper class, use `Throwable.addSuppressed()` to attach the cleanup exception to the primary exception. * If only the primary block fails, throw that exception directly to satisfy the caller's `catch` requirements. * If both the primary block and the cleanup fail, throw the primary exception and add the cleanup exception as a suppressed error. * If only the cleanup fails, it is then appropriate to throw the cleanup exception as the standalone failure. ### Considerations for Checked and Unchecked Exceptions The impact of exception handling varies by language, particularly in Java where checked exceptions are enforced by the compiler. * Converting a checked exception into an unchecked `RuntimeException` inside a wrapper can cause the compiler to miss necessary error-handling requirements. * If exceptions have parent-child relationships, such as `IOException` and `Exception`, wrapping can cause a specific handler to be bypassed in favor of a more generic one. * It is generally recommended to only wrap checked exceptions in `RuntimeException` when the error is truly unrecoverable and the caller is not expected to handle it. When designing custom resource management utilities, always evaluate which exception is most critical for the caller to see. Prioritize the primary execution error and use suppression for auxiliary cleanup failures to ensure that your error-handling remains transparent and predictable for the rest of the application.

line

Code Quality Improvement Techniques Part (opens in new tab)

The Null Object Pattern is a design technique that replaces null values with objects representing "empty" or "invalid" states to simplify code and provide functional fallbacks. While it effectively streamlines logic for collections and general data flows, using it when error conditions must be explicitly distinguished can lead to hidden bugs and reduced type safety. Developers should generally prefer statically verified types, such as Optionals or language-native nullable types, unless the error case can be seamlessly integrated into the happy-path logic. ### Benefits of the Null Object Pattern * **Code Simplification:** By returning an empty list or a "null object" instead of a literal `null`, callers can avoid repetitive null-check boilerplate. * **Functional Continuity:** It allows for uninterrupted processing in functional chains, such as using `.asSequence().map().forEach()`, because the "empty" object still satisfies the required interface. * **Fallback Provisioning:** The pattern is useful for converting errors into safe fallback values, such as displaying an "Unknown User" profile image rather than crashing or requiring complex conditional UI logic. ### Risks of Silent Failures and Logic Errors * **Bypassing Compiler Safety:** Unlike nullable types in Kotlin or Swift, which force developers to handle the `null` case, a custom null object (e.g., `UserModel.INVALID`) allows code to compile even if the developer forgets to check the object's validity. * **Identity vs. Equivalence:** Implementing the pattern requires caution regarding how the object is compared. If a null object is checked via reference identity (`==`) but the class lacks a proper `equals` implementation, new instances with the same "empty" values may not be recognized as invalid. * **Debugging Difficulty:** When a null object is used inappropriately, the program may continue to run with dummy data. This makes bugs harder to detect compared to a runtime error or a compile-time type mismatch. ### Best Practices for Type Safeness * **Prefer Static Verification:** When boundary conditions or errors must be handled differently than the "happy path," use `Optional`, `Maybe`, or native nullable types to ensure the compiler enforces error handling. * **Criteria for Use:** Reserve the Null Object Pattern for cases where the error logic is identical to the normal logic, or when multiple "empty" candidates exist that cannot be easily resolved through static typing. * **Runtime Errors as a Tool:** In dynamic or non-nullable contexts, a runtime error is often preferable to silent execution with an invalid object, as it provides a clear signal that an unexpected state has been reached. ### Recommendation To maintain high code quality, utilize the Null Object Pattern primarily for collections and UI fallbacks. For core business logic where the presence of data is critical, rely on type-safe mechanisms that force explicit handling of missing values, thereby preventing invalid states from propagating silently through the system.