|
1 | 1 | # Checked Exceptions for C# |
2 | 2 |
|
3 | | -**Take control of exception flow — enforce explicit handling or declaration in C#** |
| 3 | +Take control of exception flow—enforce explicit handling or declaration in C#. |
4 | 4 |
|
5 | | -[](…) [](https://www.nuget.org/packages/Sundstrom.CheckedExceptions/)  |
6 | | - |
7 | | -[❓ FAQ](#user-content--frequently-asked-questions-faq) • [🧪 Sample project](https://github.com/marinasundstrom/CheckedExceptions/blob/main/SampleProject/Program.cs) • [📚 Documentation](docs) • [📝 Change Log](CHANGELOG.md) |
8 | | - |
9 | | ---- |
10 | | - |
11 | | -## Demo |
12 | | - |
13 | | -_Click the image to watch the [video](https://www.youtube.com/watch?v=ldJjMrqB8X4) om YouTube._ |
14 | | - |
15 | | -<a href="https://www.youtube.com/watch?v=ldJjMrqB8X4"><img src="screenshots/Screenshot3.png" alt="Video" width="500" /></a> |
16 | | - |
17 | | -There are other videos in [this playlist](https://www.youtube.com/playlist?list=PLLBU--06ftFpmZWhQExVDXcefWol1i0zq). |
18 | | - |
19 | | ---- |
20 | | - |
21 | | -## 🚀 What It Does |
22 | | - |
23 | | -CheckedExceptions is a Roslyn analyzer that makes exception handling **explicit** and reveals how exceptions propagate through your code. |
24 | | - |
25 | | -If a method might throw an exception, the caller must either: |
26 | | - |
27 | | -* 🧯 Handle it (with `try/catch`), or |
28 | | -* 📣 Declare it (with `[Throws(typeof(...))]`) |
29 | | - |
30 | | -✅ Inspired by Java’s checked exceptions<br /> |
31 | | -⚙️ Fully opt-in<br /> |
32 | | -💡 Analyzer warnings by default — can be elevated to errors<br /> |
33 | | -📄 Supports .NET and third-party libraries via XML documentation<br /> |
34 | | -🛠 Includes code fixes to help you quickly handle or declare exceptions<br /> |
35 | | -➕ Supports .NET Standard 2.1<br /> |
36 | | - |
37 | | ---- |
38 | | - |
39 | | -## ✅ Quick Example |
| 5 | +```bash |
| 6 | +dotnet add package Sundstrom.CheckedExceptions |
| 7 | +``` |
40 | 8 |
|
41 | 9 | ```csharp |
42 | 10 | public class Sample |
43 | 11 | { |
44 | 12 | public void Execute() |
45 | 13 | { |
46 | | - // ⚠️ THROW001: Unhandled exception type 'InvalidOperationException' |
47 | | - Perform(); |
| 14 | + Perform(); // ⚠️ THROW001: Unhandled InvalidOperationException |
48 | 15 | } |
49 | 16 |
|
50 | 17 | [Throws(typeof(InvalidOperationException))] |
51 | | - public void Perform() |
52 | | - { |
53 | | - throw new InvalidOperationException("Oops!"); |
54 | | - } |
55 | | -} |
56 | | -``` |
57 | | - |
58 | | -✔️ Fix it by **handling**: |
59 | | - |
60 | | -```csharp |
61 | | -public void Execute() |
62 | | -{ |
63 | | - try { Perform(); } |
64 | | - catch (InvalidOperationException) { /* handle */ } |
65 | | -} |
66 | | -``` |
67 | | - |
68 | | -Or by **declaring**: |
69 | | - |
70 | | -```csharp |
71 | | -[Throws(typeof(InvalidOperationException))] |
72 | | -public void Execute() |
73 | | -{ |
74 | | - Perform(); |
75 | | -} |
76 | | -``` |
77 | | - |
78 | | ---- |
79 | | - |
80 | | -## 🧠 Why Use It? |
81 | | - |
82 | | -- Avoid silent exception propagation |
83 | | -- Document intent with `[Throws]` instead of comments |
84 | | -- Enforce better error design across your codebase |
85 | | -- Works with unannotated .NET methods via XML docs |
86 | | -- Plays nice with nullable annotations |
87 | | -- Avoid confusing [Throws] with `<exception>` — enforce contracts, not just documentation |
88 | | - |
89 | | ---- |
90 | | - |
91 | | -## 📦 Installation |
92 | | - |
93 | | -```bash |
94 | | -dotnet add package Sundstrom.CheckedExceptions |
95 | | -``` |
96 | | - |
97 | | -And define `ThrowsAttribute` in your project: |
98 | | - |
99 | | -```csharp |
100 | | -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Delegate | AttributeTargets.Property, AllowMultiple = true)] |
101 | | -public class ThrowsAttribute : Attribute |
102 | | -{ |
103 | | - public List<Type> ExceptionTypes { get; } = new(); |
104 | | - public ThrowsAttribute(Type exceptionType, params Type[] others) { … } |
105 | | -} |
106 | | -``` |
107 | | - |
108 | | -Find the full definition [here](https://github.com/marinasundstrom/CheckedExceptions/blob/main/CheckedExceptions.Attribute/ThrowsAttribute.cs). |
109 | | - |
110 | | ---- |
111 | | - |
112 | | -## ⚙️ Configuration |
113 | | - |
114 | | -### .editorconfig |
115 | | - |
116 | | -```ini |
117 | | -dotnet_diagnostic.THROW001.severity = warning |
118 | | -dotnet_diagnostic.THROW003.severity = warning |
119 | | -``` |
120 | | - |
121 | | -### `.csproj` |
122 | | - |
123 | | -```xml |
124 | | -<PropertyGroup> |
125 | | - <WarningsAsErrors>nullable,THROW001</WarningsAsErrors> |
126 | | -</PropertyGroup> |
127 | | -``` |
128 | | - |
129 | | -### JSON Settings |
130 | | - |
131 | | -Add `CheckedExceptions.settings.json`: |
132 | | - |
133 | | -```json |
134 | | -{ |
135 | | - // Exceptions to completely ignore during analysis (Glob pattern). |
136 | | - "ignoredExceptions": [ |
137 | | - "System.*", |
138 | | - "System.ArgumentNullException", |
139 | | - "!System.InvalidOperationException" |
140 | | - ], |
141 | | - |
142 | | - // Exceptions to ignore but still report as informational diagnostics. |
143 | | - "informationalExceptions": { |
144 | | - "System.IO.IOException": "Propagation", |
145 | | - "System.TimeoutException": "Always" |
146 | | - }, |
147 | | - |
148 | | - // If true, exceptions will not be read from XML documentation (default: false). |
149 | | - "disableXmlDocInterop": false, |
150 | | - |
151 | | - // If true, analysis on LINQ constructs will be disabled (default: false). |
152 | | - "disableLinqSupport": false, |
153 | | - |
154 | | - // If true, exceptions in LINQ lambdas do not have to be declared (default: false). |
155 | | - "disableLinqImplicitlyDeclaredExceptions": false, |
156 | | - |
157 | | - // If true, no diagnostics will be issued on contract boundaries, such as arguments to methods and return statements (default: false). |
158 | | - "disableLinqEnumerableBoundaryWarnings": false, |
159 | | - |
160 | | - // If true, control flow analysis, with redundancy checks, is disabled (default: false). |
161 | | - "disableControlFlowAnalysis": false, |
162 | | - |
163 | | - // If true, basic redundancy checks are available when control flow analysis is disabled (default: false). |
164 | | - "enableLegacyRedundancyChecks": false, |
165 | | - |
166 | | - // If true, the analyzer will not warn about declaring base type Exception with [Throws] (default: false). |
167 | | - "disableBaseExceptionDeclaredDiagnostic": false, |
168 | | - |
169 | | - // If true, the analyzer will not warn about throwing base type Exception (default: false). |
170 | | - // Set to true if you use another analyzer reporting a similar diagnostic. |
171 | | - "disableBaseExceptionThrownDiagnostic": false |
| 18 | + public void Perform() => throw new InvalidOperationException(); |
172 | 19 | } |
173 | 20 | ``` |
174 | 21 |
|
175 | | -> **Control flow analysis** powers redundancy checks (e.g. unreachable code, redundant catches, unused exception declarations). |
176 | | -> Disabling it may improve analyzer performance slightly at the cost of precision. |
177 | | -
|
178 | | -Register in `.csproj`: |
179 | | - |
180 | | -```xml |
181 | | -<ItemGroup> |
182 | | - <AdditionalFiles Include="CheckedExceptions.settings.json" /> |
183 | | -</ItemGroup> |
184 | | -``` |
185 | | - |
186 | | ---- |
187 | | - |
188 | | -## 🔍 Diagnostics |
189 | | - |
190 | | -| ID | Message | |
191 | | -|------------|-------------------------------------------------------------------------| |
192 | | -| `THROW001` | ❗ Unhandled exception: must be caught or declared | |
193 | | -| `THROW002` | ℹ️ Ignored exception may cause runtime issues | |
194 | | -| `THROW003` | 🚫 Avoid declaring exception type `System.Exception` | |
195 | | -| `THROW004` | 🚫 Avoid throwing exception base type `System.Exception` | |
196 | | -| `THROW005` | 🔁 Duplicate declarations of the same exception type in `[Throws]` | |
197 | | -| `THROW006` | 🧬 Incompatible Throws declaration (not declared on base member) | |
198 | | -| `THROW007` | 🧬 Missing Throws declaration for base member's exception | |
199 | | -| `THROW008` | 📦 Exception already handled by declaration of super type in `[Throws]` | |
200 | | -| `THROW009` | 🧹 Redundant catch typed clause | |
201 | | -| `THROW010` | ⚠️ `[Throws]` is not valid on full property declarations | |
202 | | -| `THROW011` | 📄 Exception in XML docs is not declared with `[Throws]` | |
203 | | -| `THROW012` | 🧹 Redundant exception declaration (declared but never thrown) | |
204 | | -| `THROW013` | 🧹 Redundant catch-all clause (no remaining exceptions to catch) | |
205 | | -| `THROW014` | 🧹 Catch clause has no remaining exceptions to handle | |
206 | | -| `THROW015` | 🧹 Catch clause is redundant (General diagnostic) | |
207 | | -| `THROW020` | 🛑 Unreachable code detected | |
208 | | -| `IDE001` | 🙈 Unreachable code (hidden diagnostic for editor greying) | |
209 | | - |
210 | | -## 🛠 Code Fixes |
211 | | - |
212 | | -The analyzer offers the following automated code fixes: |
213 | | - |
214 | | -* ✅ **Add exception declaration** |
215 | | - Adds `[Throws(typeof(...))]` attribute to declare the exception, or appends exception to existing attribute. |
216 | | - *(Fixes `THROW001`)* |
217 | | - |
218 | | -* 🧯 **Surround with try/catch** |
219 | | - Wraps the throwing site in a `try`/`catch` block. |
220 | | - *(Fixes `THROW001`)* |
221 | | - |
222 | | -* ➕ **Add catch to existing try block** |
223 | | - Appends a new `catch` clause to an existing `try`. |
224 | | - *(Fixes `THROW001`)* |
225 | | - |
226 | | -* 🧹 **Remove redundant catch clause** |
227 | | - Removes a catch clause for an exception type that is never thrown. |
228 | | - *(Fixes `THROW009`, `THROW013`, `THROW014`)* |
229 | | - |
230 | | -* 🔧 **Add `[Throws]` declaration from base member** |
231 | | - Ensures overridden or implemented members declare the same exceptions as their base/interface. |
232 | | - *(Fixes `THROW007`)* |
233 | | - |
234 | | -* 🔧 **Add `[Throws]` declaration from XML doc** |
235 | | - Converts `<exception>` XML documentation into `[Throws]` attributes. |
236 | | - *(Fixes `THROW011`)* |
237 | | - |
238 | | -* ➕ **Introduce catch clauses from rethrow in catch-all** |
239 | | - Appends new `catch` clauses before "catch all". |
240 | | - *(Fixes `THROW001`)* |
241 | | - |
242 | | ---- |
243 | | - |
244 | | -## ✨ Advanced Features |
245 | | - |
246 | | -* **Lambdas, local functions, accessors, events** – full support across member kinds |
247 | | -* **Exception inheritance analysis** – understands base/derived exception relationships |
248 | | -* **XML documentation interop** – merges `[Throws]` with `<exception>` tags from external libraries |
249 | | -* **Nullability awareness** – respects `#nullable enable` context |
250 | | -* **Standard library knowledge** – recognizes common framework exceptions (e.g. `Console.WriteLine` → `IOException`) |
251 | | -* **Control flow analysis** – detects whether exceptions are reachable, flags redundant `catch` clauses, and reports unreachable code caused by prior throws or returns |
252 | | - |
253 | | ---- |
254 | | - |
255 | | -## ❓ Frequently Asked Questions (FAQ) |
256 | | - |
257 | | -### ❓ How is this different from Java's checked exceptions? |
258 | | - |
259 | | -**Answer:** |
260 | | - |
261 | | -Java's checked exceptions are **mandatory** — the compiler enforces them, and every method must declare or handle them. While this promotes visibility, it also leads to friction, boilerplate, and workarounds like `throws Exception`. |
262 | | - |
263 | | -This analyzer takes a **modern, flexible approach**: |
264 | | - |
265 | | -* ⚠️ **Warnings by default**, not errors — you’re in control. |
266 | | -* ✍️ **Opt-in declaration** using `[Throws]` — only where it matters. |
267 | | -* 🛠️ **Code fixes and suppression** make adoption practical. |
268 | | -* 🔄 **Gradual adoption** — use it for new code, leave legacy code untouched. |
269 | | -* 🎯 **Focused on intention**, not obligation — you declare what callers need to know, not what `int.Parse` might throw. |
270 | | - |
271 | | -> ✅ Summary: |
272 | | -> This is *exception design with intent*, not enforcement by force. It improves exception hygiene without the rigidity of Java’s model. |
273 | | -
|
274 | | -### ❓ Can I use `<exception>` XML documentation tags instead of the `[Throws]` attribute? |
275 | | - |
276 | | -**Answer:** |
277 | | - |
278 | | -No — for your own code, `<exception>` tags are **not treated as semantic declarations** by the analyzer. While they are useful for documentation and IntelliSense, they are not part of the C# language’s type system and cannot be reliably analyzed or enforced. |
279 | | - |
280 | | -Instead, we encourage and require the use of the `[Throws]` attribute for declaring exceptions in a way that is: |
281 | | - |
282 | | -- Explicit and machine-readable |
283 | | -- Suitable for static analysis and enforcement |
284 | | -- Integrated with code fixes and tooling support |
285 | | - |
286 | | -#### 🧩 Interoperability with external libraries |
287 | | - |
288 | | -When analyzing external APIs (e.g., referenced .NET assemblies), we **do** recognize `<exception>` tags from their XML documentation — but only for **interop purposes**. That is: |
289 | | - |
290 | | -- We treat documented exceptions from public APIs as "declared" when `[Throws]` is not available. |
291 | | -- This helps maintain compatibility without requiring upstream changes. |
292 | | - |
293 | | -> ⚠️ Summary: |
294 | | -> `<exception>` tags are respected for **interop**, but they are **not a replacement** for `[Throws]` in code you control. |
295 | | -
|
296 | | -### ❓ What about .NET Standard 2.0 support? |
297 | | - |
298 | | -**Answer:** |
299 | | - |
300 | | -The analyzer offers **limited support** for projects targeting .NET Standard 2.0. You’ll still get accurate diagnostics for your own code, as well as third-party libraries. However, members defined in the .NET Standard framework may not indicate which exceptions they throw. |
301 | | - |
302 | | -This is due to a **technical limitation**: the XML documentation files for .NET Standard assemblies are often incomplete or malformed, making it impossible to extract reliable exception information. |
303 | | - |
304 | | -✅ **Recommendation:** Target a modern .NET SDK (e.g., .NET 6 or later) to get full analyzer support, including framework exception annotations. |
305 | | - |
306 | | -### ❓ What about LINQ support? |
307 | | - |
308 | | -**Answer:** |
309 | | - |
310 | | -There is initial support for LINQ query operators. |
311 | | - |
312 | | -```csharp |
313 | | -List<string> values = [ "10", "20", "abc", "30" ]; |
314 | | - |
315 | | -var tens = values |
316 | | - .Where(s => int.Parse(s) % 10 is 0) |
317 | | - .ToArray(); // THROW001: unhandled FormatException, OverflowException |
318 | | -``` |
319 | | - |
320 | | -> Exceptions are inferred and implicit on LINQ methods, so no declarations needed. this behavior can be disabled. |
321 | | -
|
322 | | -Read about it [here](docs/linq-support.md). |
323 | | - |
324 | | ---- |
325 | | - |
326 | | -## 🤝 Contributing |
327 | | - |
328 | | -1. Fork |
329 | | -2. Create feature branch |
330 | | -3. Push PR with tests & documentation |
331 | | -4. ❤️ |
332 | | - |
333 | | ---- |
334 | | - |
335 | | -## 📜 License |
| 22 | +For full documentation, samples, and FAQ, see the [project repository](https://github.com/marinasundstrom/CheckedExceptions). |
336 | 23 |
|
337 | | -[MIT](LICENSE) |
0 commit comments