The Exceptionator

Roslyn analyzers for improving exception handling in C# code.
This project includes a set of diagnostics aimed at encouraging better exception practices. Each rule detects common pitfalls and helps improve reliability, maintainability, and clarity of your exception logic.
Rules
EX001: Exception should include a message
Ensures that exceptions are instantiated with a meaningful message.
❌ Bad:
throw new InvalidOperationException();
✅ Good:
throw new InvalidOperationException("Operation failed due to ...");
EX002: Avoid throwing base exceptions
Avoids throwing base exceptions like System.Exception or System.SystemException.
❌ Bad:
throw new Exception("Something went wrong");
✅ Good:
throw new InvalidOperationException("Something went wrong");
EX003: Missing inner exception
Ensures that newly thrown exceptions inside catch blocks include the original exception as inner exception.
❌ Bad:
catch (Exception ex)
{
throw new CustomException("Something failed");
}
✅ Good:
catch (Exception ex)
{
throw new CustomException("Something failed", ex);
}
EX004: Use 'throw;' instead of 'throw ex;'
Preserves the original stack trace when rethrowing exceptions.
❌ Bad:
catch (Exception ex)
{
throw ex;
}
✅ Good:
catch (Exception ex)
{
throw;
}
EX005: Exception variable is unused
Detects catch variables that are never used.
❌ Bad:
catch (Exception ex) { LogError(); }
✅ Good:
catch (Exception ex) { LogError(ex); }
EX006: Unreachable code after throw
Detects code written after a throw statement that will never execute.
❌ Bad:
throw new Exception();
DoSomething();
✅ Good:
DoSomething();
throw new Exception();
EX007: Pointless try/catch block
Detects try/catch blocks that don't add meaningful handling logic.
❌ Bad:
try
{
DoSomething();
}
catch (Exception)
{
throw;
}
✅ Good:
DoSomething();
EX008: ThreadAbortException must not be swallowed
Ensures ThreadAbortException is either rethrown or reset.
❌ Bad:
catch (ThreadAbortException ex) { LogError(); }
✅ Good:
catch (ThreadAbortException ex) { Thread.ResetAbort(); }
EX009: Empty try block with catch
Detects try blocks that are empty while having a catch.
❌ Bad:
try { } catch (Exception ex) { Log(ex); }
✅ Good:
try { DoSomething(); } catch (Exception ex) { Log(ex); }
EX010: Task.WaitAll should be wrapped with AggregateException catch
Ensures proper exception handling for Task.WaitAll by requiring AggregateException catch or using Task.WhenAll.
❌ Bad:
try { Task.WaitAll(tasks); } catch (Exception ex) { Log(ex); }
✅ Good:
try { Task.WaitAll(tasks); } catch (AggregateException ex) { Log(ex); }
EX011: Empty catch block
Detects catch blocks that are completely empty without even a comment.
❌ Bad:
try { DoSomething(); } catch (Exception) { }
✅ Good:
try { DoSomething(); } catch (Exception) { /* intentionally ignored */ }
EX012: Don't throw exceptions from property getters
Discourages throwing exceptions directly from property getters.
❌ Bad:
public string Name => throw new Exception();
✅ Good:
public string Name => _name ?? "";
EX013: Avoid throwing ex.InnerException
Detects when ex.InnerException is thrown directly, which may cause null reference issues and loses the stack trace.
❌ Bad:
catch (Exception ex) { throw ex.InnerException; }
✅ Good:
catch (Exception ex) { throw; }
EX014: Avoid logging only ex.Message
Suggests logging the full exception instead of just the message to retain full context.
❌ Bad:
LogError(ex.Message);
✅ Good:
LogError(ex);
EX015: Avoid logging ex.ToString()
Recommends logging the exception directly rather than calling ToString.
❌ Bad:
LogError("Error: " + ex.ToString());
✅ Good:
LogError(ex);
EX016: Avoid throwing null – use a specific exception type instead.
❌ Bad:
throw null;
✅ Good:
throw new InvalidOperationException("An error");
EX017: Avoid when clauses that always evaluate to true
Detects when filters on catch blocks that always return true and are thus redundant.
❌ Bad:
catch (Exception ex) when (true) { Handle(ex); }
✅ Good:
catch (Exception ex) when (ex is IOException) { Handle(ex); }
EX018: Filter exceptions manually inside catch
Detects catch blocks that catch a broad exception and then manually filter using if (ex is ...). Prefer catch filters (when) or catching specific exception types instead.
❌ Bad:
try { ... }
catch (Exception ex)
{
if (ex is ArgumentException)
HandleArgument();
}
✅ Good:
try { ... }
catch (ArgumentException)
{
HandleArgument();
}
EX019: NotImplementedException left in code
Detects throw new NotImplementedException() left in methods or properties.
❌ Bad:
public void DoWork() => throw new NotImplementedException();
✅ Good:
public void DoWork() => ActualImplementation();
EX020: Exception class should be public
Ensures that exception types are declared public to be visible when thrown or caught across assemblies.
❌ Bad:
class CustomException : Exception
{
}
✅ Good:
public class CustomException : Exception
{
}
EX021: Missing expected constructors on custom exception
Ensures that custom exceptions implement the expected constructors with message and inner exception parameters.
❌ Bad:
public class MyCustomException : Exception
{
}
✅ Good:
public class MyCustomException : Exception
{
public MyCustomException(string message) : base(message) { }
public MyCustomException(string message, Exception innerException)
: base(message, innerException) { }
}
EX022: Exception constructors must call base
Ensures that exception constructors pass their parameters (message, innerException) to the base constructor.
❌ Bad:
public MyCustomException(string message) { }
public MyCustomException(string message, Exception inner) { }
✅ Good:
public MyCustomException(string message) : base(message) { }
public MyCustomException(string message, Exception inner) : base(message, inner) { }
EX023: Exception class name must end with 'Exception'
Ensures consistency and clarity by requiring exception classes to follow the naming convention of ending with 'Exception'.
❌ Bad:
public class MyCustomError : Exception
{
}
✅ Good:
public class MyCustomException : Exception
{
}
EX024: Avoid catching fatal exceptions like StackOverflowException or ExecutionEngineException
Flags catch blocks that handle fatal exceptions which should not be caught or are uncatchable.
❌ Bad:
try { ... }
catch (StackOverflowException ex) { Log(ex); }
✅ Good:
try { ... }
catch (Exception ex) { Log(ex); }
EX025: Handle documented exceptions or propagate them explicitly
Flags calls to methods that document thrown exceptions (via <exception> XML comments) when the caller does not handle or document those exceptions.
❌ Bad:
public void Method1()
{
Method2(); // Method2 documents that it may throw NullReferenceException
}
/// <summary>
/// <exception cref="System.NullReferenceException"/>
/// </summary>
public void Method2()
{
var x = 1 / 0;
}
✅ Good:
public void Method1()
{
try
{
Method2();
}
catch (NullReferenceException ex)
{
throw;
}
}
/// <summary>
/// <exception cref="System.NullReferenceException"/>
/// </summary>
public void Method2()
{
var x = 1 / 0;
}
Flags methods that already document one or more exceptions using <exception> XML tags but forget to document additional exceptions that are explicitly thrown within the method body.
❌ Bad:
/// <summary>
/// Does something.
/// </summary>
/// <exception cref="System.InvalidOperationException"/>
public void M()
{
throw new InvalidOperationException();
throw new ArgumentException("arg"); // not documented
}
✅ Good:
/// <summary>
/// Does something.
/// </summary>
/// <exception cref="System.InvalidOperationException"/>
/// <exception cref="System.ArgumentException"/>
public void M()
{
throw new InvalidOperationException();
throw new ArgumentException("arg");
}
Acknowledgments
Sponsored by elmah.io.