|
| 1 | +# Exception Handling: Best Practices |
| 2 | + |
| 3 | +This document outlines best practices for managing exceptions in your C# code, emphasizing clarity, performance, and maintainability. These principles are designed to work in tandem with the CheckedExceptions Analyzer. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## Table of Contents |
| 8 | + |
| 9 | +1. [Understand What a Bug Is](#understand-what-a-bug-is) |
| 10 | +2. [What Exceptions Represent](#what-exceptions-represent) |
| 11 | +3. [When to Avoid Exceptions](#when-to-avoid-exceptions) |
| 12 | +4. [How to Handle Exceptions Well](#how-to-handle-exceptions-well) |
| 13 | +5. [Limiting and Containing Exceptions](#limiting-and-containing-exceptions) |
| 14 | +6. [Exception Type Guidelines](#exception-type-guidelines) |
| 15 | +7. [Alternatives to Exceptions](#alternatives-to-exceptions) |
| 16 | +8. [Conclusion](#conclusion) |
| 17 | + |
| 18 | +--- |
| 19 | + |
| 20 | +## Understand What a Bug Is |
| 21 | + |
| 22 | +A **bug** is unexpected behavior. It defies user or developer expectations and can surface at any stage in development or production. |
| 23 | + |
| 24 | +> **Example:** |
| 25 | +> |
| 26 | +> ```csharp |
| 27 | +> public void Foo() |
| 28 | +> { |
| 29 | +> throw new InvalidOperationException(); // Unhandled => Bug |
| 30 | +> } |
| 31 | +> ``` |
| 32 | +
|
| 33 | +Unhandled exceptions are **bugs**. Every exception that escapes a method and isn't accounted for should be treated as a failure to anticipate or contain an error condition. |
| 34 | +
|
| 35 | +--- |
| 36 | +
|
| 37 | +## What Exceptions Represent |
| 38 | +
|
| 39 | +Exceptions are not just errors—they are **exceptional**. They signal that something **unexpected or unlikely** has happened that can't be handled by regular logic. |
| 40 | +
|
| 41 | +Examples include: |
| 42 | +
|
| 43 | +* Hardware faults (e.g., I/O errors, memory access violations) |
| 44 | +* Arithmetic overflows |
| 45 | +* System limitations |
| 46 | +
|
| 47 | +> **Note:** Exceptions differ from expected domain errors, such as validation failures, which are better modeled using `Result` objects or alternative mechanisms. |
| 48 | +
|
| 49 | +--- |
| 50 | +
|
| 51 | +## When to Avoid Exceptions |
| 52 | +
|
| 53 | +### 1. Avoid Declaring Exceptions |
| 54 | +
|
| 55 | +Use `ThrowsAttribute` if needed, but avoid **over-declaring** exceptions—especially in public APIs. It communicates unpredictability and complexity to callers. |
| 56 | +
|
| 57 | +### 2. Avoid Using Exceptions for Control Flow |
| 58 | +
|
| 59 | +Use non-throwing APIs when available: |
| 60 | +
|
| 61 | +> **Preferred:** |
| 62 | +> |
| 63 | +> ```csharp |
| 64 | +> if (int.TryParse(input, out var result)) { ... } |
| 65 | +> ``` |
| 66 | +> |
| 67 | +> **Avoid:** |
| 68 | +> |
| 69 | +> ```csharp |
| 70 | +> int result = int.Parse(input); // Throws on failure |
| 71 | +> ``` |
| 72 | +
|
| 73 | +--- |
| 74 | +
|
| 75 | +## How to Handle Exceptions Well |
| 76 | +
|
| 77 | +### 1. Catch Exceptions Close to the Source |
| 78 | +
|
| 79 | +Catching exceptions near where they are thrown reduces the cost of stack unwinding and improves clarity. |
| 80 | +
|
| 81 | +> **Example:** |
| 82 | +> |
| 83 | +> ```csharp |
| 84 | +> try |
| 85 | +> { |
| 86 | +> SaveToDisk(); |
| 87 | +> } |
| 88 | +> catch (IOException ex) |
| 89 | +> { |
| 90 | +> LogError(ex); |
| 91 | +> Retry(); |
| 92 | +> } |
| 93 | +> ``` |
| 94 | +
|
| 95 | +### 2. Avoid Top-Level Catch-Alls |
| 96 | +
|
| 97 | +Catching all exceptions globally hides bugs and encourages ignoring faults. |
| 98 | +
|
| 99 | +> **Anti-pattern:** |
| 100 | +> |
| 101 | +> ```csharp |
| 102 | +> try |
| 103 | +> { |
| 104 | +> RunApplication(); |
| 105 | +> } |
| 106 | +> catch (Exception) |
| 107 | +> { |
| 108 | +> // Swallowed! Don't do this unless you're logging & exiting |
| 109 | +> } |
| 110 | +> ``` |
| 111 | +
|
| 112 | +--- |
| 113 | +
|
| 114 | +## Limiting and Containing Exceptions |
| 115 | +
|
| 116 | +### 1. Avoid Throwing More Than Four Exception Types per Method |
| 117 | +
|
| 118 | +Too many exceptions make it hard to reason about possible failure paths. |
| 119 | +
|
| 120 | +### 2. Catch and Wrap as Domain Exception |
| 121 | +
|
| 122 | +Aggregate multiple exception scenarios under a clearly named `DomainException`: |
| 123 | +
|
| 124 | +> ```csharp |
| 125 | +> catch (IOException ex) when (ex.Message.Contains("disk")) |
| 126 | +> { |
| 127 | +> throw new StorageUnavailableException("Disk error", ex); |
| 128 | +> } |
| 129 | +> ``` |
| 130 | +
|
| 131 | +Still, prefer `Result` objects for recoverable, expected issues. |
| 132 | +
|
| 133 | +--- |
| 134 | +
|
| 135 | +## Exception Type Guidelines |
| 136 | +
|
| 137 | +### ✅ Reuse .NET Framework Exceptions: |
| 138 | +
|
| 139 | +* `InvalidOperationException` |
| 140 | +* `ArgumentNullException` |
| 141 | +* `ArgumentOutOfRangeException` |
| 142 | +
|
| 143 | +These are conventional and understood by most developers. |
| 144 | +
|
| 145 | +### 🚫 Avoid Reusing Internal/System Exceptions: |
| 146 | +
|
| 147 | +Do **not** throw: |
| 148 | +
|
| 149 | +* `NullReferenceException` |
| 150 | +* `StackOverflowException` |
| 151 | +* `AccessViolationException` |
| 152 | +
|
| 153 | +These are meant for the runtime. |
| 154 | +
|
| 155 | +### 🚫 Avoid AggregateException |
| 156 | +
|
| 157 | +Use a custom, descriptive wrapper instead. `AggregateException` is difficult to pattern match and leads to boilerplate. |
| 158 | +
|
| 159 | +### 🚫 Avoid Exception Type Hierarchies |
| 160 | +
|
| 161 | +Stick to **flat**, explicitly named exception classes when custom exceptions are needed. |
| 162 | +
|
| 163 | +> **Avoid:** |
| 164 | +> |
| 165 | +> ```csharp |
| 166 | +> public class MyBaseException : Exception { } |
| 167 | +> public class FooException : MyBaseException { } |
| 168 | +> public class BarException : MyBaseException { } |
| 169 | +> ``` |
| 170 | +> |
| 171 | +> **Prefer:** |
| 172 | +> |
| 173 | +> ```csharp |
| 174 | +> public class UserAccountLockedException : Exception { } |
| 175 | +> ``` |
| 176 | +
|
| 177 | +### 🚫 Avoid Catching Base Type `Exception` |
| 178 | +
|
| 179 | +Too broad and may mask unexpected issues: |
| 180 | +
|
| 181 | +> ```csharp |
| 182 | +> catch (Exception) { ... } // Try to avoid |
| 183 | +> ``` |
| 184 | +
|
| 185 | +--- |
| 186 | +
|
| 187 | +## Alternatives to Exceptions |
| 188 | +
|
| 189 | +### 1. Prefer Result Objects for Domain Logic |
| 190 | +
|
| 191 | +> **Example:** |
| 192 | +> |
| 193 | +> ```csharp |
| 194 | +> public record Result(bool Success, string? Error); |
| 195 | +> |
| 196 | +> public Result RegisterUser(string name) |
| 197 | +> { |
| 198 | +> if (string.IsNullOrWhiteSpace(name)) |
| 199 | +> return new(false, "Name is required"); |
| 200 | +> |
| 201 | +> return new(true, null); |
| 202 | +> } |
| 203 | +> ``` |
| 204 | +
|
| 205 | +### 2. Nullable Reference Types |
| 206 | +
|
| 207 | +Leverage the compiler’s nullable analysis to prevent `ArgumentNullException`s. |
| 208 | +
|
| 209 | +--- |
| 210 | +
|
| 211 | +## Conclusion |
| 212 | +
|
| 213 | +Exceptions should remain what they were intended to be—**exceptional**. By minimizing their use, limiting their propagation, and favoring explicit, predictable error signaling mechanisms like `Result`, you write code that’s easier to maintain, safer to extend, and more aligned with user expectations. |
| 214 | +
|
| 215 | +Together with the **CheckedExceptions Analyzer**, these practices help ensure that exceptions remain manageable and that your software becomes less exception-prone and less buggy. |
0 commit comments