Show HN: C# analyzer for error handling patterns in your including call graph
1 day ago
1
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
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
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:
Detects any method or member that contains throw statements.
// BeforevoidProcessData(stringdata){if(string.IsNullOrEmpty(data))thrownewArgumentException("Data cannot be empty");}// After (Code Fix Applied)voidProcessData(stringdata){try{if(string.IsNullOrEmpty(data))thrownewArgumentException("Data cannot be empty");}catch(ArgumentExceptionex){// Handle exceptionthrow;}}
THROWS002: Unhandled throw statement
Severity: Warning | Code Fix: Wrap in try-catch
Detects throw statements not wrapped in try-catch blocks.
// BeforevoidSaveFile(stringpath,stringcontent){File.WriteAllText(path,content);// Throws IOException}// After (Code Fix Applied)voidSaveFile(stringpath,stringcontent){try{File.WriteAllText(path,content);}catch(IOExceptionex){// Handle exceptionthrow;}}
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.
Detects catch clauses that can never be reached due to ordering.
// Before - WRONG (InvalidOperationException is unreachable)try{DoSomething();}catch(Exceptionex)// ❌ Catches everything{Log(ex);}catch(InvalidOperationExceptionex)// Never reached{LogSpecific(ex);}// After (Code Fix Applied) - CORRECTtry{DoSomething();}catch(InvalidOperationExceptionex)// ✓ Specific first{LogSpecific(ex);}catch(Exceptionex){Log(ex);}
THROWS008: Empty catch block
Severity: Warning | Code Fix: Add logging or remove
Detects empty catch blocks that silently swallow exceptions.
// Before - WRONGtry{LoadConfiguration();}catch(Exception){// ❌ Empty catch swallows exceptions}// After (Code Fix: Add Logging)try{LoadConfiguration();}catch(Exceptionex){Logger.LogError(ex,"Failed to load configuration");// ✓ Logs errorthrow;}
THROWS009: Catch block only rethrows
Severity: Info | Code Fix: Remove unnecessary catch
Detects catch blocks that only rethrow without doing any work.
// Before - Unnecessarytry{ProcessData();}catch(Exceptionex){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 broadtry{ParseUserInput(input);}catch(Exceptionex)// ❌ Catches everything{LogError(ex);}// After (Code Fix: Add Filter)try{ParseUserInput(input);}catch(Exceptionex)when(exisFormatException||exisArgumentException)// ✓ 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.
// DetectedvoidProcessFile(stringpath){varcontent=File.ReadAllText(path);// May throw IOExceptionProcess(content);}// RecommendedvoidProcessFile(stringpath){try{varcontent=File.ReadAllText(path);Process(content);}catch(IOExceptionex){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 documentationpublicvoidValidateUser(stringusername){if(string.IsNullOrEmpty(username))thrownewArgumentException("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>publicvoidValidateUser(stringusername){if(string.IsNullOrEmpty(username))thrownewArgumentException("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)asyncTask<string>LoadDataAsync(stringid){if(string.IsNullOrEmpty(id))thrownewArgumentException();// ❌ Synchronous throwreturnawaitLoadFromDatabaseAsync(id);}// After - CORRECTasyncTask<string>LoadDataAsync(stringid){if(string.IsNullOrEmpty(id))returnTask.FromException<string>(newArgumentException());// ✓ Returns faulted taskreturnawaitLoadFromDatabaseAsync(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)asyncvoidLoadDataButton_Click(objectsender,EventArgse){awaitLoadDataAsync();// ❌ Exception crashes app}// After (Code Fix Applied) - CORRECTasyncTaskLoadDataButton_Click(objectsender,EventArgse){try{awaitLoadDataAsync();// ✓ Exception can be handled}catch(Exceptionex){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)voidProcessData(){LoadDataAsync();// ❌ Exception lost}// After (Code Fix Applied) - CORRECTasyncTaskProcessData(){awaitLoadDataAsync();// ✓ 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(intcount){if(count<0)thrownewArgumentException();// ❌ Thrown during enumerationfor(inti=0;i<count;i++)yieldreturni;}// After (Code Fix Applied) - CORRECTIEnumerable<int>GetNumbers(intcount){if(count<0)thrownewArgumentException();// ✓ Thrown immediatelyreturnGetNumbersIterator(count);}IEnumerable<int>GetNumbersIterator(intcount){for(inti=0;i<count;i++)yieldreturni;}
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)varresults=items.Select(x =>{if(x==null)thrownewArgumentNullException();// ❌ Crashes enumerationreturnx.Value;});// After (Code Fix Applied) - CORRECTvarresults=items.Select(x =>{try{if(x==null)thrownewArgumentNullException();returnx.Value;}catch(ArgumentNullExceptionex){Logger.LogError(ex,"Null item in collection");returndefault;}});
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)=>{thrownewInvalidOperationException();// ❌ Crashes app};// After (Code Fix Applied) - CORRECTbutton.Click+=(sender,e)=>{try{ProcessClick();}catch(InvalidOperationExceptionex){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{varuser=FindUser(id);if(user==null)thrownewUserNotFoundException();// ❌ Expected condition}catch(UserNotFoundException){CreateDefaultUser(id);}// After (Code Fix Applied) - CORRECTvaruser=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 - WRONGpublicclassUserNotFound:Exception{}// ❌ Missing "Exception"// After (Code Fix Applied) - CORRECTpublicclassUserNotFoundException:Exception{}// ✓ Follows convention
// Before - WRONG (exception in loop)for(inti=0;i<items.Count;i++){if(items[i]==null)thrownewArgumentNullException();// ❌ Performance issueProcess(items[i]);}// After (Code Fix Applied) - CORRECT// Validate before loopfor(inti=0;i<items.Count;i++){if(items[i]==null)continue;// ✓ Or validate before loop startsProcess(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 failurespublicUserParseUser(stringdata){if(string.IsNullOrEmpty(data))thrownewFormatException();// Expected conditionreturnJsonSerializer.Deserialize<User>(data);}// After - Using Result<T> pattern (suggested)publicResult<User>ParseUser(stringdata){if(string.IsNullOrEmpty(data))returnResult<User>.Failure("Data cannot be empty");try{returnResult<User>.Success(JsonSerializer.Deserialize<User>(data));}catch(JsonExceptionex){returnResult<User>.Failure(ex.Message);}}
For comprehensive examples demonstrating all diagnostics and code fixes, see:
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.)
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 nupkg
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
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.