ThrowsAnalyzer
A Roslyn-based C# analyzer that detects exception handling patterns in your code. ThrowsAnalyzer helps identify throw statements, unhandled exceptions, and try-catch blocks across all executable member types.
Build Status & NuGet Packages
| Package | Version | Downloads | Build |
|---|---|---|---|
| ThrowsAnalyzer | |||
| ThrowsAnalyzer.Cli | |||
| RoslynAnalyzer.Core |
Repository Contents
This repository includes three packages:
- ThrowsAnalyzer - Comprehensive exception analysis with 30 diagnostics and 16 code fixes
- ThrowsAnalyzer.Cli - Command-line tool for analyzing projects and generating reports
- RoslynAnalyzer.Core - Reusable infrastructure for building custom Roslyn analyzers
Features
ThrowsAnalyzer provides 30 diagnostic rules organized into 6 categories, with 16 automated code fixes for quick issue resolution.
Diagnostic Rules Summary
| Category | Diagnostics | Description |
|---|---|---|
| Basic Exception Handling | THROWS001-003, 004, 007-010 | Fundamental exception patterns and anti-patterns |
| Exception Flow Analysis | THROWS017-019 | Method call exception propagation and documentation |
| Async Exception Patterns | THROWS020-022 | Async/await exception handling issues |
| Iterator Exception Patterns | THROWS023-024 | Exception handling in yield-based iterators |
| Lambda Exception Patterns | THROWS025-026 | Exception handling in lambda expressions |
| Best Practices | THROWS027-030 | Design patterns and performance recommendations |
Code Fixes Summary
| Code Fix | Diagnostics | Actions |
|---|---|---|
| Wrap in try-catch | THROWS001, THROWS002 | Adds try-catch around throwing code |
| Fix rethrow | THROWS004 | Converts throw ex; to throw; |
| Reorder catches | THROWS007 | Reorders catch clauses from specific to general |
| Add/Remove logging | THROWS008, THROWS003 | Adds logging or removes empty catch |
| Remove rethrow-only catch | THROWS009 | Removes unnecessary catch blocks |
| Add exception filter | THROWS010 | Adds when clause to specific catches |
| Convert async void | THROWS021 | Converts async void to async Task |
| Add Task observation | THROWS022 | Adds await or continuation |
| Wrap iterator validation | THROWS023 | Moves validation outside iterator |
| Add try-finally | THROWS024 | Adds try-finally for cleanup |
| Wrap lambda in try-catch | THROWS025, THROWS026 | Adds exception handling to lambdas |
| Refactor control flow | THROWS027 | Suggests return value instead of exceptions |
| Rename exception | THROWS028 | Renames to follow convention |
| Move to cold path | THROWS029 | Suggests refactoring for performance |
| Add XML docs | THROWS019 | Documents thrown exceptions |
| Suggest Result pattern | THROWS030 | Suggests Result for error handling |
Supported Member Types
ThrowsAnalyzer analyzes exception handling patterns in:
- Methods
- Constructors and Destructors
- Properties (including expression-bodied properties)
- Property Accessors (get, set, init, add, remove)
- Operators (binary, unary, conversion)
- Local Functions
- Lambda Expressions (simple and parenthesized)
- Anonymous Methods
Installation
Analyzer Library
Add the analyzer to your project via NuGet:
dotnet add package ThrowsAnalyzer
Once installed, the analyzer runs automatically during compilation. Diagnostics will appear in your IDE and build output.
CLI Tool
Install the command-line tool globally to analyze projects and generate reports:
dotnet tool install --global ThrowsAnalyzer.Cli
CLI Quick Start
# Analyze a project and generate reports throws-analyzer analyze MyProject.csproj # Analyze a solution throws-analyzer analyze MySolution.sln # Generate HTML and Markdown reports throws-analyzer analyze MyProject.csproj --verbose --open
The CLI tool generates comprehensive reports showing:
- Summary statistics by diagnostic ID, project, severity, and file
- Interactive HTML reports with sortable tables
- Markdown reports for documentation
- Detailed diagnostics with code snippets
See CLI Tool Documentation for complete usage guide and CI/CD integration examples.
Configuration
ThrowsAnalyzer provides granular configuration options through .editorconfig files. You can control analyzer enablement, severity, and which member types to analyze.
Enabling/Disabling Individual Analyzers
Control whether each analyzer is completely enabled or disabled:
[*.cs] # Enable/disable throw statement analyzer (THROWS001) throws_analyzer_enable_throw_statement = true # Enable/disable unhandled throw analyzer (THROWS002) throws_analyzer_enable_unhandled_throw = true # Enable/disable try-catch block analyzer (THROWS003) throws_analyzer_enable_try_catch = true
All analyzers are enabled by default. Setting an option to false completely disables that analyzer, regardless of severity settings.
Configuring Analyzer Severity
Control the severity of each diagnostic rule:
[*.cs] # Basic analyzers # THROWS001: Detects throw statements in members dotnet_diagnostic.THROWS001.severity = suggestion # THROWS002: Detects unhandled throw statements (not wrapped in try-catch) dotnet_diagnostic.THROWS002.severity = warning # THROWS003: Detects try-catch blocks in members dotnet_diagnostic.THROWS003.severity = suggestion # Advanced type-aware analyzers # THROWS004: Rethrow anti-pattern (throw ex; instead of throw;) dotnet_diagnostic.THROWS004.severity = warning # THROWS007: Unreachable catch clause due to ordering dotnet_diagnostic.THROWS007.severity = warning # THROWS008: Empty catch block swallows exceptions dotnet_diagnostic.THROWS008.severity = warning # THROWS009: Catch block only rethrows exception dotnet_diagnostic.THROWS009.severity = suggestion # THROWS010: Overly broad exception catch dotnet_diagnostic.THROWS010.severity = suggestion
Severity options: none, silent, suggestion, warning, error
Configuring Member Type Analysis
Selectively enable or disable analysis for specific member types:
[*.cs] # Analyze regular methods throws_analyzer_analyze_methods = true # Analyze constructors throws_analyzer_analyze_constructors = true # Analyze destructors/finalizers throws_analyzer_analyze_destructors = true # Analyze operator overloads throws_analyzer_analyze_operators = true # Analyze conversion operators (implicit/explicit) throws_analyzer_analyze_conversion_operators = true # Analyze properties (expression-bodied properties) throws_analyzer_analyze_properties = true # Analyze property accessors (get, set, init, add, remove) throws_analyzer_analyze_accessors = true # Analyze local functions throws_analyzer_analyze_local_functions = true # Analyze lambda expressions throws_analyzer_analyze_lambdas = true # Analyze anonymous methods (delegate { } syntax) throws_analyzer_analyze_anonymous_methods = true
All member types are analyzed by default. Set any option to false to disable analysis for that member type.
Example Configurations
Minimal Configuration (Methods and Constructors Only)
[*.cs] throws_analyzer_analyze_methods = true throws_analyzer_analyze_constructors = true throws_analyzer_analyze_destructors = false throws_analyzer_analyze_operators = false throws_analyzer_analyze_conversion_operators = false throws_analyzer_analyze_properties = false throws_analyzer_analyze_accessors = false throws_analyzer_analyze_local_functions = false throws_analyzer_analyze_lambdas = false throws_analyzer_analyze_anonymous_methods = false
Focus on Unhandled Exceptions Only
[*.cs] throws_analyzer_enable_throw_statement = false throws_analyzer_enable_unhandled_throw = true throws_analyzer_enable_try_catch = false dotnet_diagnostic.THROWS002.severity = error
Disable Analysis for Lambdas and Local Functions
[*.cs] throws_analyzer_analyze_local_functions = false throws_analyzer_analyze_lambdas = false throws_analyzer_analyze_anonymous_methods = false
See .editorconfig.example for a complete configuration template.
Complete Diagnostic Reference
Category 1: Basic Exception Handling (8 rules)
THROWS001: Method contains throw statement
Severity: Info | Code Fix: Wrap in try-catch
Detects any method or member that contains throw statements.
// Before void ProcessData(string data) { if (string.IsNullOrEmpty(data)) throw new ArgumentException("Data cannot be empty"); } // After (Code Fix Applied) void ProcessData(string data) { try { if (string.IsNullOrEmpty(data)) throw new ArgumentException("Data cannot be empty"); } catch (ArgumentException ex) { // Handle exception throw; } }
THROWS002: Unhandled throw statement
Severity: Warning | Code Fix: Wrap in try-catch
Detects throw statements not wrapped in try-catch blocks.
// Before void SaveFile(string path, string content) { File.WriteAllText(path, content); // Throws IOException } // After (Code Fix Applied) void SaveFile(string path, string content) { try { File.WriteAllText(path, content); } catch (IOException ex) { // Handle exception throw; } }
THROWS003: Method contains try-catch block
Severity: Info | Code Fix: Remove try-catch or add logging
Flags methods containing try-catch blocks for tracking exception handling.
THROWS004: Rethrow anti-pattern
Severity: Warning | Code Fix: Fix rethrow
Detects throw ex; which resets the stack trace. Should use throw; instead.
// Before - WRONG (resets stack trace) try { DoSomething(); } catch (Exception ex) { throw ex; // ❌ Resets stack trace } // After (Code Fix Applied) - CORRECT try { DoSomething(); } catch (Exception ex) { throw; // ✓ Preserves stack trace }
THROWS007: Unreachable catch clause
Severity: Warning | Code Fix: Reorder catches
Detects catch clauses that can never be reached due to ordering.
// Before - WRONG (InvalidOperationException is unreachable) try { DoSomething(); } catch (Exception ex) // ❌ Catches everything { Log(ex); } catch (InvalidOperationException ex) // Never reached { LogSpecific(ex); } // After (Code Fix Applied) - CORRECT try { DoSomething(); } catch (InvalidOperationException ex) // ✓ Specific first { LogSpecific(ex); } catch (Exception ex) { Log(ex); }
THROWS008: Empty catch block
Severity: Warning | Code Fix: Add logging or remove
Detects empty catch blocks that silently swallow exceptions.
// Before - WRONG try { LoadConfiguration(); } catch (Exception) { // ❌ Empty catch swallows exceptions } // After (Code Fix: Add Logging) try { LoadConfiguration(); } catch (Exception ex) { Logger.LogError(ex, "Failed to load configuration"); // ✓ Logs error throw; }
THROWS009: Catch block only rethrows
Severity: Info | Code Fix: Remove unnecessary catch
Detects catch blocks that only rethrow without doing any work.
// Before - Unnecessary try { ProcessData(); } catch (Exception ex) { throw; // No work done, catch is unnecessary } // After (Code Fix Applied) ProcessData(); // ✓ Simplified
THROWS010: Overly broad exception catch
Severity: Info | Code Fix: Add exception filter
Detects catching System.Exception or System.SystemException.
// Before - Too broad try { ParseUserInput(input); } catch (Exception ex) // ❌ Catches everything { LogError(ex); } // After (Code Fix: Add Filter) try { ParseUserInput(input); } catch (Exception ex) when (ex is FormatException || ex is ArgumentException) // ✓ Specific { LogError(ex); }
Category 2: Exception Flow Analysis (3 rules)
THROWS017: Unhandled method call
Severity: Info
Detects method calls that may throw exceptions without try-catch handling.
// Detected void ProcessFile(string path) { var content = File.ReadAllText(path); // May throw IOException Process(content); } // Recommended void ProcessFile(string path) { try { var content = File.ReadAllText(path); Process(content); } catch (IOException ex) { Logger.LogError(ex, "Failed to read file: {Path}", path); throw; } }
THROWS018: Deep exception propagation
Severity: Info
Detects exceptions propagating through many call stack levels.
THROWS019: Undocumented public API exception
Severity: Warning | Code Fix: Add XML documentation
Detects public methods that throw exceptions without XML documentation.
// Before - Missing documentation public void ValidateUser(string username) { if (string.IsNullOrEmpty(username)) throw new ArgumentException("Username required"); } // After (Code Fix Applied) /// <summary> /// Validates the specified username. /// </summary> /// <param name="username">The username to validate.</param> /// <exception cref="ArgumentException"> /// Thrown when <paramref name="username"/> is null or empty. /// </exception> public void ValidateUser(string username) { if (string.IsNullOrEmpty(username)) throw new ArgumentException("Username required"); }
Category 3: Async Exception Patterns (3 rules)
THROWS020: Async method throws synchronously
Severity: Warning
Detects async methods that throw exceptions before the first await.
// Before - WRONG (throws before async) async Task<string> LoadDataAsync(string id) { if (string.IsNullOrEmpty(id)) throw new ArgumentException(); // ❌ Synchronous throw return await LoadFromDatabaseAsync(id); } // After - CORRECT async Task<string> LoadDataAsync(string id) { if (string.IsNullOrEmpty(id)) return Task.FromException<string>( new ArgumentException()); // ✓ Returns faulted task return await LoadFromDatabaseAsync(id); }
THROWS021: Async void exception
Severity: Error | Code Fix: Convert to async Task
Detects async void methods that can crash the application if they throw.
// Before - WRONG (can crash app) async void LoadDataButton_Click(object sender, EventArgs e) { await LoadDataAsync(); // ❌ Exception crashes app } // After (Code Fix Applied) - CORRECT async Task LoadDataButton_Click(object sender, EventArgs e) { try { await LoadDataAsync(); // ✓ Exception can be handled } catch (Exception ex) { ShowError(ex.Message); } }
THROWS022: Unobserved Task exception
Severity: Warning | Code Fix: Add await or continuation
Detects Task-returning methods called without await or exception handling.
// Before - WRONG (exception unobserved) void ProcessData() { LoadDataAsync(); // ❌ Exception lost } // After (Code Fix Applied) - CORRECT async Task ProcessData() { await LoadDataAsync(); // ✓ Exception propagates }
Category 4: Iterator Exception Patterns (2 rules)
THROWS023: Iterator deferred exception
Severity: Info | Code Fix: Move validation outside iterator
Detects exceptions in yield-based iterators that are deferred until enumeration.
// Before - WRONG (exception deferred) IEnumerable<int> GetNumbers(int count) { if (count < 0) throw new ArgumentException(); // ❌ Thrown during enumeration for (int i = 0; i < count; i++) yield return i; } // After (Code Fix Applied) - CORRECT IEnumerable<int> GetNumbers(int count) { if (count < 0) throw new ArgumentException(); // ✓ Thrown immediately return GetNumbersIterator(count); } IEnumerable<int> GetNumbersIterator(int count) { for (int i = 0; i < count; i++) yield return i; }
THROWS024: Iterator try-finally issue
Severity: Warning | Code Fix: Add proper cleanup
Detects try-finally issues in iterators where finally may not execute.
Category 5: Lambda Exception Patterns (2 rules)
THROWS025: Lambda uncaught exception
Severity: Warning | Code Fix: Wrap in try-catch
Detects lambdas that throw exceptions without proper handling.
// Before - WRONG (exception propagates to LINQ) var results = items.Select(x => { if (x == null) throw new ArgumentNullException(); // ❌ Crashes enumeration return x.Value; }); // After (Code Fix Applied) - CORRECT var results = items.Select(x => { try { if (x == null) throw new ArgumentNullException(); return x.Value; } catch (ArgumentNullException ex) { Logger.LogError(ex, "Null item in collection"); return default; } });
THROWS026: Event handler lambda exception
Severity: Error | Code Fix: Wrap in try-catch
Detects event handler lambdas that throw unhandled exceptions.
// Before - WRONG (can crash app) button.Click += (sender, e) => { throw new InvalidOperationException(); // ❌ Crashes app }; // After (Code Fix Applied) - CORRECT button.Click += (sender, e) => { try { ProcessClick(); } catch (InvalidOperationException ex) { MessageBox.Show(ex.Message); // ✓ Handled gracefully } };
Category 6: Best Practices (4 rules)
THROWS027: Exception used for control flow
Severity: Info | Code Fix: Refactor to use return values
Detects exceptions used for normal control flow instead of return values.
// Before - WRONG (exception for control flow) try { var user = FindUser(id); if (user == null) throw new UserNotFoundException(); // ❌ Expected condition } catch (UserNotFoundException) { CreateDefaultUser(id); } // After (Code Fix Applied) - CORRECT var user = FindUser(id); if (user == null) // ✓ Return value check { CreateDefaultUser(id); }
THROWS028: Custom exception naming violation
Severity: Info | Code Fix: Rename exception
Detects custom exception types not ending with "Exception".
// Before - WRONG public class UserNotFound : Exception { } // ❌ Missing "Exception" // After (Code Fix Applied) - CORRECT public class UserNotFoundException : Exception { } // ✓ Follows convention
THROWS029: Exception in hot path
Severity: Warning | Code Fix: Suggest refactoring
Detects exceptions thrown inside loops (performance issue).
// Before - WRONG (exception in loop) for (int i = 0; i < items.Count; i++) { if (items[i] == null) throw new ArgumentNullException(); // ❌ Performance issue Process(items[i]); } // After (Code Fix Applied) - CORRECT // Validate before loop for (int i = 0; i < items.Count; i++) { if (items[i] == null) continue; // ✓ Or validate before loop starts Process(items[i]); }
THROWS030: Consider Result pattern
Severity: Info | Code Fix: Suggest Result
Suggests using Result pattern for expected error conditions.
// Before - Using exceptions for expected failures public User ParseUser(string data) { if (string.IsNullOrEmpty(data)) throw new FormatException(); // Expected condition return JsonSerializer.Deserialize<User>(data); } // After - Using Result<T> pattern (suggested) public Result<User> ParseUser(string data) { if (string.IsNullOrEmpty(data)) return Result<User>.Failure("Data cannot be empty"); try { return Result<User>.Success( JsonSerializer.Deserialize<User>(data)); } catch (JsonException ex) { return Result<User>.Failure(ex.Message); } }
Examples and Samples
For comprehensive examples demonstrating all diagnostics and code fixes, see:
- ExceptionPatterns Sample - Demonstrates all 30 diagnostics
- LibraryManagement Sample - Real-world library management system
RoslynAnalyzer.Core
RoslynAnalyzer.Core is a reusable infrastructure library extracted from ThrowsAnalyzer for building custom Roslyn analyzers. It provides battle-tested components for common analyzer patterns.
Installation
dotnet add package RoslynAnalyzer.Core
Features
- Call Graph Analysis - Track method invocations with cycle detection and transitive operations
- Executable Member Detection - Identify all C# member types (methods, constructors, properties, lambdas, local functions, etc.)
- Async/Await Pattern Detection - Analyze async methods, detect async void, find awaits, and identify unawaited tasks
- Iterator Pattern Detection - Detect yield-based iterators, find yield statements, and analyze iterator flow
- Lambda Expression Analysis - Generic lambda detection with context identification (event handlers, LINQ, Task.Run, callbacks)
- Type Hierarchy Analysis - Navigate type hierarchies and check interface implementations
- Configuration Infrastructure - Read .editorconfig settings for analyzer customization
- Suppression Infrastructure - Support custom suppression attributes
- Performance Optimizations - Compilation and symbol caching with statistics
Quick Start
using RoslynAnalyzer.Core.Analysis.Patterns.Async; using RoslynAnalyzer.Core.Analysis.Patterns.Iterators; using RoslynAnalyzer.Core.Analysis.Patterns.Lambda; // Async pattern detection var asyncInfo = AsyncMethodDetector.GetAsyncMethodInfo(methodSymbol, methodNode, semanticModel); if (asyncInfo.IsAsyncVoid) { // Handle async void pattern } // Iterator pattern detection if (IteratorMethodDetector.IsIteratorMethod(methodSymbol, methodNode)) { var yieldStatements = IteratorMethodDetector.GetYieldStatements(methodBody); // Analyze iterator pattern } // Lambda pattern detection var lambdas = LambdaDetector.GetLambdaExpressions(methodBody); foreach (var lambda in lambdas) { var context = LambdaDetector.GetLambdaContext(lambda, semanticModel); if (context == LambdaContext.EventHandler) { // Handle event handler lambda } }
Documentation
For complete API reference, examples, and usage guides, see the RoslynAnalyzer.Core README.
Real-World Example
ThrowsAnalyzer itself is built using RoslynAnalyzer.Core, providing a comprehensive real-world example of how to use the library to build production-ready analyzers.
Building from Source
# Clone the repository git clone https://github.com/wieslawsoltes/ThrowsAnalyzer.git cd ThrowsAnalyzer # Build everything dotnet build # Run all tests (461 tests) dotnet test # Build individual projects dotnet build src/ThrowsAnalyzer/ThrowsAnalyzer.csproj dotnet build src/RoslynAnalyzer.Core/RoslynAnalyzer.Core.csproj # Run specific test projects dotnet test tests/ThrowsAnalyzer.Tests/ThrowsAnalyzer.Tests.csproj dotnet test tests/RoslynAnalyzer.Core.Tests/RoslynAnalyzer.Core.Tests.csproj # Create NuGet packages dotnet pack -c Release -o nupkg
Project Structure
ThrowsAnalyzer/
├── src/
│ ├── ThrowsAnalyzer/ # Main analyzer with 30 diagnostics and 16 code fixes
│ ├── ThrowsAnalyzer.Cli/ # Command-line tool for project analysis
│ └── RoslynAnalyzer.Core/ # Reusable infrastructure library
├── tests/
│ ├── ThrowsAnalyzer.Tests/ # 274 tests for ThrowsAnalyzer
│ └── RoslynAnalyzer.Core.Tests/ # 187 tests for RoslynAnalyzer.Core
├── samples/
│ ├── ExceptionPatterns/ # Demonstrates all 30 diagnostics
│ └── LibraryManagement/ # Real-world example application
└── docs/ # Documentation and guides
Contributing
Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests.
License
This project is licensed under the MIT License. See the LICENSE file for details.