Take control of exception flow β enforce explicit handling or declaration in C#
β FAQ β’ π§ͺ Sample project β’ π Documentation β’ π Change Log
Click the image to watch the video om YouTube.
There are other videos in this playlist.
CheckedExceptions is a Roslyn analyzer that makes exception handling explicit and reveals how exceptions propagate through your code.
If a method might throw an exception, the caller must either:
- π§― Handle it (with
try/catch
), or - π£ Declare it (with
[Throws(typeof(...))]
)
β
Inspired by Javaβs checked exceptions
βοΈ Fully opt-in
π‘ Analyzer warnings by default β can be elevated to errors
π Supports .NET and third-party libraries via XML documentation
π Includes code fixes to help you quickly handle or declare exceptions
β Supports .NET Standard 2.1
public class Sample
{
public void Execute()
{
// β οΈ THROW001: Unhandled exception type 'InvalidOperationException'
Perform();
}
[Throws(typeof(InvalidOperationException))]
public void Perform()
{
throw new InvalidOperationException("Oops!");
}
}
βοΈ Fix it by handling:
public void Execute()
{
try { Perform(); }
catch (InvalidOperationException) { /* handle */ }
}
Or by declaring:
[Throws(typeof(InvalidOperationException))]
public void Execute()
{
Perform();
}
- Avoid silent exception propagation
- Document intent with
[Throws]
instead of comments - Enforce better error design across your codebase
- Works with unannotated .NET methods via XML docs
- Plays nice with nullable annotations
- Avoid confusing [Throws] with
<exception>
β enforce contracts, not just documentation
dotnet add package Sundstrom.CheckedExceptions
And define ThrowsAttribute
in your project:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Delegate | AttributeTargets.Property, AllowMultiple = true)]
public class ThrowsAttribute : Attribute
{
public List<Type> ExceptionTypes { get; } = new();
public ThrowsAttribute(Type exceptionType, params Type[] others) { β¦ }
}
Find the full definition here.
dotnet_diagnostic.THROW001.severity = warning
dotnet_diagnostic.THROW003.severity = warning
<PropertyGroup>
<WarningsAsErrors>nullable,THROW001</WarningsAsErrors>
</PropertyGroup>
A baseline template is available in default-settings.json
. For guidance on choosing the right default classification for your project, see the configuration guide.
The analyzer reads a single exceptions
dictionary that explicitly classifies each exception type as Ignored
, Informational
, NonStrict
, or Strict
. Any exception not listed defaults to NonStrict
, so an unclassified throw will trigger a low-severity diagnostic but won't require [Throws]
or a catch
. NonStrict
exceptions remain part of analysisβyou may still declare or catch them, and doing so isn't considered redundant. Only exceptions classified as Ignored
are completely filtered out.
Add CheckedExceptions.settings.json
:
{
// Default classification for exceptions not listed (default: NonStrict)
"defaultExceptionClassification": "NonStrict",
"exceptions": {
"System.ArgumentNullException": "Ignored",
"System.IO.IOException": "Informational",
"System.TimeoutException": "NonStrict",
"System.Exception": "Strict"
},
// If true, exceptions will not be read from XML documentation (default: false).
"disableXmlDocInterop": false,
// If true, analysis on LINQ constructs will be disabled (default: false).
"disableLinqSupport": false,
// If true, implicit exception inference in LINQ lambdas is disabled and declarations are required (default: false).
"disableLinqImplicitlyDeclaredExceptions": false,
// If true, no diagnostics will be issued on contract boundaries, such as arguments to methods and return statements (default: false).
"disableLinqEnumerableBoundaryWarnings": false,
// If true, control flow analysis, with redundancy checks, is disabled (default: false).
"disableControlFlowAnalysis": false,
// If true, basic redundancy checks are available when control flow analysis is disabled (default: false).
"enableLegacyRedundancyChecks": false,
// If true, declaring [Throws(typeof(Exception))] acts as a catch-all and suppresses hierarchy redundancy checks; the base-type
// warning (THROW003) remains unless "disableBaseExceptionDeclaredDiagnostic" is set (default: false).
"treatThrowsExceptionAsCatchRest": false,
// If true, the analyzer will not warn about declaring base type Exception with [Throws] (default: false).
"disableBaseExceptionDeclaredDiagnostic": false,
// If true, the analyzer will not warn about throwing base type Exception (default: false).
// Set to true if you use another analyzer reporting a similar diagnostic.
"disableBaseExceptionThrownDiagnostic": false
}
Migration note: Legacy
ignoredExceptions
andinformationalExceptions
settings are still recognized but have been deprecated. They are automatically translated to the unifiedexceptions
map.
Control flow analysis powers redundancy checks (e.g. unreachable code, redundant catches, unused exception declarations). Disabling it may improve analyzer performance slightly at the cost of precision.
Register in .csproj
:
<ItemGroup>
<AdditionalFiles Include="CheckedExceptions.settings.json" />
</ItemGroup>
ID | Message |
---|---|
THROW001 |
β Unhandled exception: must be caught or declared |
THROW002 |
βΉοΈ Non-strict exception may cause runtime issues |
THROW003 |
π« Avoid declaring exception type System.Exception |
THROW004 |
π« Avoid throwing exception base type System.Exception |
THROW005 |
π Duplicate declarations of the same exception type in [Throws] |
THROW006 |
𧬠Incompatible Throws declaration (not declared on base member) |
THROW007 |
𧬠Missing Throws declaration for base member's exception |
THROW008 |
π¦ Exception already handled by declaration of super type in [Throws] |
THROW009 |
π§Ή Redundant catch typed clause |
THROW010 |
[Throws] is not valid on full property declarations |
THROW011 |
π Exception in XML docs is not declared with [Throws] |
THROW012 |
π§Ή Redundant exception declaration (declared but never thrown) |
THROW013 |
π§Ή Redundant catch-all clause (no remaining exceptions to catch) |
THROW014 |
π§Ή Catch clause has no remaining exceptions to handle |
THROW015 |
π§Ή Catch clause is redundant (General diagnostic) |
THROW020 |
π Unreachable code detected |
IDE001 |
π Unreachable code (hidden diagnostic for editor greying) |
The analyzer offers the following automated code fixes:
-
β Add exception declaration Adds
[Throws(typeof(...))]
attribute to declare the exception, or appends exception to existing attribute. (FixesTHROW001
) -
π§― Surround with try/catch Wraps the throwing site in a
try
/catch
block. (FixesTHROW001
) -
β Add catch to existing try block Appends a new
catch
clause to an existingtry
. (FixesTHROW001
) -
π§Ή Remove redundant catch clause Removes a catch clause for an exception type that is never thrown. (Fixes
THROW009
,THROW013
,THROW014
) -
π§ Add
[Throws]
declaration from base member Ensures overridden or implemented members declare the same exceptions as their base/interface. (FixesTHROW007
) -
π§ Add
[Throws]
declaration from XML doc Converts<exception>
XML documentation into[Throws]
attributes. (FixesTHROW011
) -
β Introduce catch clauses from rethrow in catch-all Appends new
catch
clauses before "catch all". (FixesTHROW001
)
- Lambdas, local functions, accessors, events β full support across member kinds
- Exception inheritance analysis β understands base/derived exception relationships
- XML documentation interop β merges
[Throws]
with<exception>
tags from external libraries - Nullability awareness β respects
#nullable enable
context - Standard library knowledge β recognizes common framework exceptions (e.g.
Console.WriteLine
βIOException
) - Control flow analysis β detects whether exceptions are reachable, flags redundant
catch
clauses, and reports unreachable code caused by prior throws or returns
Answer:
Java's checked exceptions are mandatory β the compiler enforces them, and every method must declare or handle them. While this promotes visibility, it also leads to friction, boilerplate, and workarounds like throws Exception
.
This analyzer takes a modern, flexible approach:
β οΈ Warnings by default, not errors β youβre in control.- βοΈ Opt-in declaration using
[Throws]
β only where it matters. - π οΈ Code fixes and suppression make adoption practical.
- π Gradual adoption β use it for new code, leave legacy code untouched.
- π― Focused on intention, not obligation β you declare what callers need to know, not what
int.Parse
might throw.
β Summary: This is exception design with intent, not enforcement by force. It improves exception hygiene without the rigidity of Javaβs model.
Answer:
No β for your own code, <exception>
tags are not treated as semantic declarations by the analyzer. While they are useful for documentation and IntelliSense, they are not part of the C# languageβs type system and cannot be reliably analyzed or enforced.
Instead, we encourage and require the use of the [Throws]
attribute for declaring exceptions in a way that is:
- Explicit and machine-readable
- Suitable for static analysis and enforcement
- Integrated with code fixes and tooling support
When analyzing external APIs (e.g., referenced .NET assemblies), we do recognize <exception>
tags from their XML documentation β but only for interop purposes. That is:
- We treat documented exceptions from public APIs as "declared" when
[Throws]
is not available. - This helps maintain compatibility without requiring upstream changes.
β οΈ Summary:
<exception>
tags are respected for interop, but they are not a replacement for[Throws]
in code you control.
Answer:
The analyzer offers limited support for projects targeting .NET Standard 2.0. Youβll still get accurate diagnostics for your own code, as well as third-party libraries. However, members defined in the .NET Standard framework may not indicate which exceptions they throw.
This is due to a technical limitation: the XML documentation files for .NET Standard assemblies are often incomplete or malformed, making it impossible to extract reliable exception information.
β Recommendation: Target a modern .NET SDK (e.g., .NET 6 or later) to get full analyzer support, including framework exception annotations.
Answer:
There is support for LINQ query operators on IEnumerable<T>
and asynchronous operators like FirstAsync
on IAsyncEnumerable<T>
(via System.Linq.Async). Support for IQueryable<T>
is enabled by default and can be disabled via the disableLinqQueryableSupport
setting.
List<string> values = [ "10", "20", "abc", "30" ];
var tens = values
.Where(s => int.Parse(s) % 10 is 0)
.ToArray(); // THROW001: unhandled FormatException, OverflowException
Exceptions are inferred and implicit on LINQ methods. Any explicit
[Throws]
on LINQ lambdas is flagged as redundant. This behavior can be disabled.
Read about it here.
- Fork
- Create feature branch
- Push PR with tests & documentation
- β€οΈ