--- title: "Jam 07 - Exercise 2: Exception Handling" tags: - 2 ๐Ÿ“ in writing - 3 ๐Ÿงช in testing - 4 ๐Ÿฅณ done - inheritance - interfaces - polymorphism - uml - abstract-classes - junit --- <!-- markdownlint-disable line-length single-h1 no-inline-html --> <!-- markdownlint-configure-file { "ul-indent": { "indent": 4 }, "link-fragments": {"ignore_case": true} } --> {%hackmd dJZ5TulxSDKme-3fSY4Lbw %} # Exercise 2 - Exception Handling In this exercise, you'll learn about exception handling in Java and how to use it to manage error conditions in your code. You'll create custom exceptions and update the search functionality to properly handle error cases. :::info ๐Ÿ”‘ **Key Concepts** - Exception handling fundamentals - Custom exception classes - Try-catch blocks - Exception propagation - Error handling best practices ::: ## The Problem - Exercise 2 Our search functionality needs better error handling. Currently, it silently fails or returns empty results when something goes wrong. We should: 1. Create custom exceptions for search errors 2. Update the Searchable interface to handle exceptions 3. Modify implementations to throw appropriate exceptions 4. Add proper error handling in search methods 5. Write tests to verify exception behavior This will make our code more robust and provide better feedback when things go wrong. ## Understanding Exceptions Exceptions are Java's way of handling error conditions. Key concepts include: - **Exception Types** - Checked exceptions (must be handled) - Unchecked exceptions (runtime errors) - Custom exceptions (application-specific errors) - **Exception Handling** - Try-catch blocks - Throws clauses - Exception propagation - Finally blocks ### Example: Custom Exceptions Let's look at an example of a custom exception and how it's used: ```java // Example: A simple custom exception public class SearchFailedException extends Exception { public SearchFailedException() { // Default string super("Search operation failed"); } public SearchFailedException(String message) { super(message); } } // Example: A custom exception for insufficient funds that stores parameters // This is useful for throwing information up the call stack that might not otherwise be available public class InsufficientFundsException extends Exception { private final int required; private final int available; public InsufficientFundsException(int required, int available) { super(String.format("Required: $%d, Available: $%d", required, available)); this.required = required; this.available = available; } public int getRequired() { return required; } public int getAvailable() { return available; } } ``` ### Example: Using Custom Exceptions Here's how this exception might be thrown and then handled (in both ways) in code: ```java // Example 1: Throwing an exception public void transferFunds(int amount) throws InsufficientFundsException { if (amount > balance) { throw new InsufficientFundsException(amount, balance); } balance -= amount; } // Example 2: Handling exceptions public void processPayment(int amount) { try { if (amount > balance) { throw new InsufficientFundsException(amount, balance); } balance -= amount; } catch (InsufficientFundsException e) { System.out.println("Payment failed: " + e.getMessage()); // Handle the error appropriately } } // Example 3: Propagating exceptions public void withdrawFunds(int amount) throws InsufficientFundsException { if (amount > balance) { throw new InsufficientFundsException(amount, balance); } balance -= amount; } ``` ### Example: Testing Exceptions Here's how we might test code that throws exceptions. When testing exceptions, it's important to understand the flow: 1. First, the code after the `() ->` (like creating a new Thermostat in the example below) is executed. We will learn more about this `() ->` syntax later! 2. If that code throws an exception, assertThrows catches it 3. assertThrows then verifies that the caught exception is of the expected type Here's an example: ```java @Test void shouldThrowInvalidTemperatureException() { // -500ยฐC is below absolute zero (-273.15ยฐC), which is physically impossible // We're testing that creating a Thermostat with this invalid temperature // throws our custom exception assertThrows(jam12.InvalidTemperatureException.class, () -> new Thermostat(-500)); } ``` ## Your Tasks Now that you understand how custom exceptions work, you'll: 1. Create a custom `InvalidPriceException` in your `jam07` package for invalid product prices 2. Update the `Product` class to use this exception - hint 1: if the code is DRY, there is one place to throw this, otherwise there will be two places - hint 2: there will be two methods that need `throws` added to their method signature - hint 3: the class needs to throw the exception also! 3. Modify your implementations to throw or handle exceptions as appropriate. An Example of one way to handle this is below. ![ExampleChangeToRegisterDriver](https://www.dropbox.com/scl/fi/t7uanj9cdizmyrdqxawbo/ExampleChangeToRegisterDriver.png?rlkey=ova0mwl8uct10rqk30qaqrgq0&raw=1) 4. Modify your existing tests and write any additional tests you might need to verify the exception behavior. An Example of one way to handle this is below (note the `fail` method - this method comes from `org.junit.jupiter.api.Assertions.fail`). - You will come across some scope issues! How can you resolve them? - You will have to change nearly every test file! - hint: you probably already have a `testNegativePrice` method in `jam05.ProductTest` - You may need to turn an edge case test into an exception test ![ExampleChangeToRegisterDriver](https://www.dropbox.com/scl/fi/2gyqlft8tbju99zg6v0wm/ExampleChangeToProductTest.png?rlkey=0nhi2r1b0o5qwi12o5b6w84g4&raw=1) You might have noticed how one change in one file (throwing an exception) threw (no pun intended) a LOT of your other files out of wack. This is exactly why we have a robust test structure! When we make changes that affect multiple parts of our system, our tests help us find all the places that need to be updated. Without these tests, we might miss updating some files, leading to bugs in production. With our test suite, we can be confident that we've caught and fixed all the necessary changes. :::warning ๐Ÿšจ **Common Implementation Pitfalls** - Not documenting exceptions - Swallowing exceptions - Using generic Exception type - Missing try-catch blocks - Incorrect exception propagation ::: ## Save Your Work - Exercise 2 **Run all tests to verify your implementation**: ```bash # Right click on the test folder in IntelliJ # Click "Run Tests in 'csci205_jams'" # Ensure all tests pass ``` **Stage your changes**: ```bash git add src/main/java/jam05/Product.java src/main/java/jam06/Register.java src/main/java/jam06/RegisterDriver.java src/main/java/jam07/InvalidPriceException.java src/test/java/jam05/ProductCatalogTest.java src/test/java/jam05/ProductTest.java src/test/java/jam06/TransactionTest.java src/test/java/jam07/ProductCatalogTest.java src/test/java/jam07/ProductTest.java src/test/java/jam07/RegisterTest.java src/test/java/jam07/TransactionTest.java ``` **Verify what files are uncommitted**: Your files maybe different so the above command may have fixed something you changed so make sure you understand your `git status`. ```bash git status ``` **Commit your work**: ```bash git commit -m "jam07: Implement exception handling for Product and related classes" ``` :::success ๐Ÿ”‘ **Key Takeaways** - Exceptions handle error conditions - Custom exceptions provide specific error types - Try-catch blocks manage exceptions - Exception documentation is essential - Tests verify error handling :::