|
| 1 | +# Exception handling |
| 2 | + |
| 3 | +## Common Anti-Pattern |
| 4 | + |
| 5 | +### Diaper (swallowing exceptions) |
| 6 | + |
| 7 | +* Makes the needle disappear from the haystack. |
| 8 | +* Debugging is impossible (or very hard at least) |
| 9 | + |
| 10 | +```groovy |
| 11 | +try { |
| 12 | + doSomeWork(); |
| 13 | +} catch (Exception e) { |
| 14 | +} |
| 15 | +``` |
| 16 | + |
| 17 | +```groovy |
| 18 | +try { |
| 19 | + doSomeWork(); |
| 20 | +} catch (Exception e) { |
| 21 | + e.printStackTrace(); |
| 22 | +} |
| 23 | +``` |
| 24 | + |
| 25 | +The `printStackTrace()` method prints to `System.err`. Often, **`System.err` is not captured by the |
| 26 | +logs.** |
| 27 | + |
| 28 | +### Decapitation |
| 29 | + |
| 30 | +Decapitated exceptions provide less information about the systems state producing the exception. |
| 31 | +Never do it. |
| 32 | + |
| 33 | +```groovy |
| 34 | +try { |
| 35 | + doSomeWork(); |
| 36 | +} catch (Exception e) { |
| 37 | + throw new RuntimeException("my crazy method threw an exception") |
| 38 | +} |
| 39 | +``` |
| 40 | + |
| 41 | +#### Instead do: |
| 42 | + |
| 43 | +```groovy |
| 44 | +try { |
| 45 | + doSomeWork(); |
| 46 | +} catch (Exception e) { |
| 47 | + throw new RuntimeException("my crazy method threw an exception", e) |
| 48 | +} |
| 49 | +``` |
| 50 | + |
| 51 | +### Checked Exceptions |
| 52 | + |
| 53 | +* Forces caller to catch the exception. |
| 54 | +* Caller might have no way to handle it |
| 55 | +* Presents an abstraction leak as the caller needs to deal with internals of the implementation. |
| 56 | + |
| 57 | +Why should the calling code know details about the implementation? |
| 58 | + |
| 59 | +<!-- we use groogy as languate so the formatting works ;D --> |
| 60 | + |
| 61 | +```groovy |
| 62 | +public static int countUserPosts() throws IOException { |
| 63 | + Properties userProps = new Properties(); |
| 64 | + try (FileReader reader = new FileReader("my-users.properties")) { |
| 65 | + userProps.load(reader); |
| 66 | + } |
| 67 | + return userProps.getProperty("user.posts.count"); |
| 68 | +} |
| 69 | +``` |
| 70 | + |
| 71 | +The code above forces the calling code of `countUserPosts()` to handle `IOException`. |
| 72 | +How should the caller decide what to do? It does not even know that some property file is read, |
| 73 | +this is an implementation detail of the `countUserPosts()` method. |
| 74 | + |
| 75 | +Instead consider an approach like the following: |
| 76 | + |
| 77 | +<!-- we use groogy as languate so the formatting works ;D --> |
| 78 | + |
| 79 | +#### Instead do: |
| 80 | +```groovy |
| 81 | +public static int countUserPosts() { |
| 82 | + try { |
| 83 | + Properties userProps = new Properties(); |
| 84 | + try (FileReader reader = new FileReader("my-users.properties")) { |
| 85 | + userProps.load(reader); |
| 86 | + } |
| 87 | + return userProps.getProperty("user.posts.count"); |
| 88 | + } catch (IOException e) { |
| 89 | + throw new RuntimeException(e); |
| 90 | + } |
| 91 | +} |
| 92 | +``` |
| 93 | + |
| 94 | +### Log-Rethrow Anti-Pattern |
| 95 | + |
| 96 | +* Do not only log an exception and rethrow |
| 97 | +* Pollutes the log |
| 98 | +* Does not add anything but confusion |
| 99 | + |
| 100 | +```groovy |
| 101 | +public static void someMethod() { |
| 102 | + try { |
| 103 | + // ... |
| 104 | + doSomeStuff(); // e.message = Oh no! Another exception. |
| 105 | + // ... |
| 106 | + } catch (ApplicationException e) { |
| 107 | + log.error(e); |
| 108 | + throw new ApplicationException(e); |
| 109 | + } |
| 110 | +} |
| 111 | +``` |
| 112 | + |
| 113 | +When we catch an exception surely we want to be totally sure we have logged it, don't we? So let's |
| 114 | +just catch any exception and make sure. |
| 115 | + |
| 116 | +Perfect. Now we have the exception in the log 💪😃! |
| 117 | + |
| 118 | +When we inspected the log we observed "Oh no! Another exception." several times. Why, when did it |
| 119 | +occur? |
| 120 | +The confusion inceases... |
| 121 | + |
| 122 | +Later, after hours of debugging, we notice the following code: |
| 123 | + |
| 124 | +```groovy |
| 125 | +public static void anotherMethod() { |
| 126 | + try { |
| 127 | + // ... |
| 128 | + someMethod(); |
| 129 | + // ... |
| 130 | + } catch (ApplicationException e) { // the re-thrown exception |
| 131 | + log.error(e); |
| 132 | + throw e; |
| 133 | + } |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +We caught, logged and threw the same exception over again. Wow, so all the time we only had one |
| 138 | +exception and not several? |
| 139 | + |
| 140 | +Thanks to the redundant catching and logging of the same exception within the called method and |
| 141 | +after the method, reading the log file was more confusing than it had to be |
| 142 | +We would have ended up with only one exception in our log file, stating where it occurred and that |
| 143 | +it occured only once. |
| 144 | +We would have saved many hours of digging through the code. |
| 145 | +**Never** only catch an exception to **log and rethrow** it. |
| 146 | + |
| 147 | +#### Instead do: [catch exception when necessary](#catch-rethrow). |
| 148 | + |
| 149 | +### Exceptions as control flow |
| 150 | + |
| 151 | +**Exceptions as control-flow smell like a _GOTO_ instruction!**<br> |
| 152 | +They do not have any scope and can run wild in your code! |
| 153 | + |
| 154 | + |
| 155 | + |
| 156 | +* Exceptions are expensive |
| 157 | +* Exceptions indicate **exceptional** system state |
| 158 | +* catch blocks -> possibilities for _diaper_, _decapitation_, _GOTOs_ |
| 159 | + |
| 160 | +Exceptions are slow. It is cheaper to validate first and then proceed than to throw an exception and |
| 161 | +handle it. |
| 162 | +When using exceptions for control flow, programmers are forced to write catch blocks and the chance |
| 163 | +for swallowing an exception or decapitating an exception increases. Developers might forget to catch |
| 164 | +an exception or if the exception was added later, legacy code might not be aware of the possibility |
| 165 | +and miss some previous type of exception. |
| 166 | + |
| 167 | +Thus, avoid exceptions for expected or likely outcomes. For expected outcomes consider returning a |
| 168 | +meaningful result instead. (_Hint: Java Records present an easy way to implement result objects_) |
| 169 | + |
| 170 | +<!-- we use groogy as languate so the formatting works ;D --> |
| 171 | + |
| 172 | +```groovy |
| 173 | +public static void isShorterThan(String input, int maxLength) { |
| 174 | + if (input.length() >= maxLength) { |
| 175 | + throw new LengthException("The string is to long"); |
| 176 | + } |
| 177 | +} |
| 178 | +
|
| 179 | +public static void isLongerThan(String input, int minLength) { |
| 180 | + if (input.length() <= minLength) { |
| 181 | + throw new LengthException("The string is to short"); |
| 182 | + } |
| 183 | +} |
| 184 | +
|
| 185 | +public static boolean isZipCode(String code) { |
| 186 | + try { |
| 187 | + isLongerThan(code, 4); |
| 188 | + isShorterThan(code, 6); |
| 189 | + return true; |
| 190 | + } catch (LengthException e) { |
| 191 | + return false; |
| 192 | + } |
| 193 | +} |
| 194 | +``` |
| 195 | + |
| 196 | +Replacing exceptions as control flow: |
| 197 | + |
| 198 | +```groovy |
| 199 | +public static boolean isShorterThan(String input, int maxLength) { |
| 200 | + return input.length() >= maxLength; |
| 201 | +} |
| 202 | +
|
| 203 | +public static boolean isLongerThan(String input, int minLength) { |
| 204 | + return input.length() <= minLength; |
| 205 | +} |
| 206 | +
|
| 207 | +public static boolean isZipCode(String code) { |
| 208 | + return isLongerThan(code, 4) && isShorterThan(code, 6); |
| 209 | +} |
| 210 | +``` |
| 211 | + |
| 212 | +If you **reasonably expect** a value to be present when no value is present, or a variable to have a |
| 213 | +specific value, throwing exceptions is fine. For example: |
| 214 | + |
| 215 | +```groovy |
| 216 | +public static void doSomeObjectStuff(Object object) { |
| 217 | + if (object == null) { |
| 218 | + throw new NullPointerException("object is null"); |
| 219 | + } |
| 220 | + // ... some code working with object |
| 221 | +} |
| 222 | +
|
| 223 | +public static void doStuff() { |
| 224 | + Object myObject = createObject(); |
| 225 | + doSomeObjectStuff(myObject); |
| 226 | +} |
| 227 | +``` |
| 228 | + |
| 229 | +Here we assume that we are given an object in the `doSomeObjectStuff` method. If this is not the |
| 230 | +case, something is off and we panic. |
| 231 | + |
| 232 | +**Note:** In those cases we do not `catch` the exception and try |
| 233 | +to [handle it as late as possible](#throw-early-handle-late). |
| 234 | + |
| 235 | +## Good practices |
| 236 | + |
| 237 | +### Throw early, handle late |
| 238 | + |
| 239 | +When encountering exceptional circumstances, additional work should be avoided. |
| 240 | +We want the application to fail as early as possible. |
| 241 | + |
| 242 | +_**Throw before mutating state. Leave the state unaltered.**_ |
| 243 | + |
| 244 | +### When to catch exceptions |
| 245 | + |
| 246 | +Avoid catching exceptions where you cannot act on them. |
| 247 | + |
| 248 | +The diaper and the log-rethrow anti-pattern indicate a caught exception in the wrong place. |
| 249 | +There are some cases in which you want to catch exceptions. |
| 250 | + |
| 251 | +#### Catch and Resolve |
| 252 | + |
| 253 | +If you can resolve the exceptional state and return back to a normal state, you can catch the |
| 254 | +exception. |
| 255 | +However, be careful that you consider this decision wisely as it poses the question: If the |
| 256 | +exception can be resolved, is it really exceptional? |
| 257 | +If the answer is no, you should revisit your code and consider using a meaningful return value or |
| 258 | +whether to split your method with SRP instead of throwing an exception. |
| 259 | + |
| 260 | +#### Catch-Rethrow |
| 261 | + |
| 262 | +It can be useful to catch an exception, do something with it and rethrow that |
| 263 | +exception. Only catch-rethrow if you |
| 264 | + |
| 265 | +- **user-friendly message**: |
| 266 | + - wrap your exception to provide the user with a friendly and understandable message. |
| 267 | +- **unit test a thrown exception**: |
| 268 | + - write a unit test that checks for a thrown exception. |
| 269 | +- **report debug information in new message**: |
| 270 | + - wrap the exception in another exception with useful debug information in the message. |
| 271 | +- **get rid of checked exception**: |
| 272 | + - [discussed earlier](#checked-exceptions) |
| 273 | + |
| 274 | +#### Global exception handling |
| 275 | + |
| 276 | +So we never want to catch unwanted exceptions. How do we save our users from bombardment with stack |
| 277 | +traces? |
| 278 | + |
| 279 | +We learned that we should **never expose stack traces to users** as this is a security issue. |
| 280 | +So should I rather swallow exceptions and ignore |
| 281 | +the [diaper anti-pattern](#diaper-swallowing-exceptions)? |
| 282 | + |
| 283 | +As a developer, you want to **feel safe and rest assured that we can throw exceptions any time and |
| 284 | +anywhere**. |
| 285 | + |
| 286 | +The solution is to create a **application global safety net**, catching all exceptions that occur. A |
| 287 | +global exception |
| 288 | +handler! |
| 289 | +Developers can throw exceptions whenever they want without needing to ensure that they are handled |
| 290 | +and without being afraid to expose details to users of your software. |
| 291 | + |
| 292 | +A global exception handler should sit between your software and the |
| 293 | +user. [In Spring applications this can be at the REST API level](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/ExceptionHandler.html). |
| 294 | +As Vaadin computes the view server-side, we can use its exception and error handling classes as |
| 295 | +global exception handlers; |
| 296 | +See [Routing Exception Handler in Vaadin](https://vaadin.com/docs/v23/routing/exceptions) |
| 297 | +and [Custom Exception Handler in Vaadin](https://vaadin.com/docs/v23/routing/exceptions#custom-exception-handlers). |
| 298 | + |
| 299 | +## Scenarios |
| 300 | + |
| 301 | +##### I want to display an error message at the point where the user entered information. |
| 302 | + |
| 303 | +There are a couple of problems you need to address: |
| 304 | + |
| 305 | +1. How can I identify the input location at the place where the error is detected? |
| 306 | +2. How expected is the invalid input? How frequently does it occur? |
| 307 | + - If it is expected: Why did I decide against an explicit response object? |
| 308 | + - If it is unexpected, how can I show a specific error message? |
| 309 | +3. What is the closest point where I can show the error message? |
| 310 | +4. Catch or do a `Result.of` call? |
| 311 | + |
| 312 | +##### I want to re-direct to a specific error page for certain errors. |
| 313 | + |
| 314 | +When does re-direction make sense? Is it only when navigating, that redirecting to a certain error |
| 315 | +page makes sense, or are the other cases? |
| 316 | +A different handler can take care of exceptions during navigation. (Does it make sense to subclass |
| 317 | +the main exception?) |
| 318 | + |
| 319 | +##### I want to display an error message at a specific location no matter where the exception occurred. |
| 320 | + |
| 321 | +At the owner of that location catch and modify ui. |
0 commit comments