Exhaustion Analyzer
The Exhaustion analyzer is a Roslyn analyzer that ensures your switch expressions handle all possible cases of discriminated unions at compile time.
The Problem
C#'s default switch exhaustiveness checking is limited. This code compiles but crashes at runtime:
// This compiles without warnings!
var output = result switch
{
OkPost(var post) => $"Success: {post.Title}",
ErrorPost(ResponseErrorPost(var err, var status, _)) => $"Error {status}",
// Missing ExceptionErrorPost case - runtime crash!
};
When an ExceptionError occurs, you get a MatchException at runtime. In production. On a Friday afternoon.
The Solution
With Exhaustion installed, the same code produces a compile error:
error EXHAUSTION001: Switch on Result is not exhaustive;
Matched: Ok<Post, HttpError<ErrorResponse>>, Error<Post, HttpError<ErrorResponse>> with ErrorResponseError<ErrorResponse>
Missing: Error<Post, HttpError<ErrorResponse>> with ExceptionError<ErrorResponse>
Runtime crashes become compile-time errors.
Installation
With RestClient.Net
Exhaustion is automatically included:
dotnet add package RestClient.Net
Standalone
For use with your own discriminated unions:
dotnet add package Exhaustion
How It Works
Exhaustion uses Roslyn's analyzer API to:
- Detect switch expressions on Result types
- Analyze which patterns are matched
- Determine which cases are missing
- Report compile-time errors for incomplete switches
Supported Types
Exhaustion works with:
Result<TSuccess, TError>from RestClient.NetHttpError<TError>from RestClient.Net- Custom discriminated unions following the same pattern
Error Messages
EXHAUSTION001: Non-exhaustive switch
error EXHAUSTION001: Switch on Result is not exhaustive;
Matched: Ok<User, HttpError<ApiError>>
Missing: Error<User, HttpError<ApiError>> with ErrorResponseError, Error<User, HttpError<ApiError>> with ExceptionError
The error tells you:
- What type the switch is on
- Which cases ARE matched
- Which cases are MISSING
Examples
Missing HttpError Cases
// ERROR: Missing both error cases
var message = result switch
{
OkUser(var user) => user.Name,
};
// ERROR: Missing ExceptionError
var message = result switch
{
OkUser(var user) => user.Name,
ErrorUser(ResponseErrorUser(var err, _, _)) => err.Message,
};
// CORRECT: All cases handled
var message = result switch
{
OkUser(var user) => user.Name,
ErrorUser(ResponseErrorUser(var err, _, _)) => err.Message,
ErrorUser(ExceptionErrorUser(var ex)) => ex.Message,
};
Using Discard Pattern
You can use discard for cases you want to handle uniformly:
// CORRECT: Discard covers remaining cases
var message = result switch
{
OkUser(var user) => user.Name,
_ => "Error occurred",
};
However, this defeats the purpose of exhaustiveness checking. Use it sparingly.
Multiple Result Types
Each Result type needs its own handling:
var (userResult, ordersResult) = await (GetUserAsync(), GetOrdersAsync());
// Both must be exhaustively handled
var userMessage = userResult switch
{
OkUser(var user) => user.Name,
ErrorUser(ResponseErrorUser(var err, _, _)) => err.Message,
ErrorUser(ExceptionErrorUser(var ex)) => ex.Message,
};
var ordersMessage = ordersResult switch
{
OkOrders(var orders) => $"{orders.Count} orders",
ErrorOrders(ResponseErrorOrders(var err, _, _)) => err.Message,
ErrorOrders(ExceptionErrorOrders(var ex)) => ex.Message,
};
Creating Exhaustive Custom Types
You can use Exhaustion with your own discriminated unions:
// Define a discriminated union
public abstract record PaymentResult
{
public sealed record Success(string TransactionId) : PaymentResult;
public sealed record Declined(string Reason) : PaymentResult;
public sealed record Error(Exception Exception) : PaymentResult;
}
// Exhaustion will check switches on PaymentResult
var message = paymentResult switch
{
PaymentResult.Success(var txId) => $"Paid: {txId}",
PaymentResult.Declined(var reason) => $"Declined: {reason}",
PaymentResult.Error(var ex) => $"Error: {ex.Message}",
};
Configuration
Suppress for Specific Lines
In rare cases, suppress the warning:
#pragma warning disable EXHAUSTION001
var message = result switch
{
OkUser(var user) => user.Name,
// Intentionally incomplete
};
#pragma warning restore EXHAUSTION001
Global Suppression
In .editorconfig (not recommended):
[*.cs]
dotnet_diagnostic.EXHAUSTION001.severity = none
IDE Integration
Visual Studio
Exhaustion errors appear in the Error List with full details. Click the error to navigate to the problematic switch.
VS Code
With the C# extension, errors appear in the Problems panel and as red squiggles in the editor.
Rider
JetBrains Rider fully supports Roslyn analyzers. Errors appear in the inspection results.
Benefits
- Catch errors early - Compile time, not runtime
- Safer refactoring - Add a new case? Compiler finds all switches
- Self-documenting - The types show all possible outcomes
- Better code review - Reviewers don't need to check for missing cases
Best Practices
- Never suppress without a comment explaining why
- Avoid discard patterns (
_) unless intentional - Handle each case explicitly for clarity
- Use type aliases for cleaner pattern matching
- Let the analyzer guide you when types change
Troubleshooting
Analyzer Not Working
Ensure the package is installed:
dotnet list package | grep Exhaustion
Rebuild the project:
dotnet build --no-incremental
False Positives
If Exhaustion reports an error incorrectly, please report an issue.
Next Steps
- Error Handling - Result type patterns
- Basic Usage - Getting started guide
- API Reference - Result type documentation