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.
| Package | Version | Downloads | Build |
|---|---|---|---|
| ThrowsAnalyzer | |||
| ThrowsAnalyzer.Cli | |||
| RoslynAnalyzer.Core |
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
ThrowsAnalyzer provides 30 diagnostic rules organized into 6 categories, with 16 automated code fixes for quick issue resolution.
| 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 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 |
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
Add the analyzer to your project via NuGet:
dotnet add package ThrowsAnalyzerOnce installed, the analyzer runs automatically during compilation. Diagnostics will appear in your IDE and build output.
Install the command-line tool globally to analyze projects and generate reports:
dotnet tool install --global ThrowsAnalyzer.Cli# 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 --openThe 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.
ThrowsAnalyzer provides granular configuration options through .editorconfig files. You can control analyzer enablement, severity, and which member types to analyze.
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 = trueAll analyzers are enabled by default. Setting an option to false completely disables that analyzer, regardless of severity settings.
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 = suggestionSeverity options: none, silent, suggestion, warning, error
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 = trueAll member types are analyzed by default. Set any option to false to disable analysis for that member type.
[*.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[*.cs]
throws_analyzer_enable_throw_statement = false
throws_analyzer_enable_unhandled_throw = true
throws_analyzer_enable_try_catch = false
dotnet_diagnostic.THROWS002.severity = error[*.cs]
throws_analyzer_analyze_local_functions = false
throws_analyzer_analyze_lambdas = false
throws_analyzer_analyze_anonymous_methods = falseSee .editorconfig.example for a complete configuration template.
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;
}
}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;
}
}Severity: Info | Code Fix: Remove try-catch or add logging
Flags methods containing try-catch blocks for tracking exception handling.
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
}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);
}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;
}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(); // ✓ SimplifiedSeverity: 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);
}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;
}
}Severity: Info
Detects exceptions propagating through many call stack levels.
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");
}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);
}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);
}
}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
}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;
}Severity: Warning | Code Fix: Add proper cleanup
Detects try-finally issues in iterators where finally may not execute.
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;
}
});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
}
};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);
}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 conventionSeverity: 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]);
}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);
}
}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 is a reusable infrastructure library extracted from ThrowsAnalyzer for building custom Roslyn analyzers. It provides battle-tested components for common analyzer patterns.
dotnet add package RoslynAnalyzer.Core- 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
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
}
}For complete API reference, examples, and usage guides, see the RoslynAnalyzer.Core README.
ThrowsAnalyzer itself is built using RoslynAnalyzer.Core, providing a comprehensive real-world example of how to use the library to build production-ready analyzers.
# 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 nupkgThrowsAnalyzer/
├── 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
Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests.
This project is licensed under the MIT License. See the LICENSE file for details.