Exceptions and Disposables
These methods help at both ends of error handling. The exception helpers read an exception chain: every message on one line, the first inner exception of a given type, the innermost cause. They also rethrow a stored exception without losing its stack trace. The disposable helpers turn an Action into an IDisposable and dispose a group of items so that one failure does not stop the others.
Both classes live in the System namespace, so you do not need a using directive.
At a glance
| Method | What it does |
|---|---|
GetAllMessages | Joins the messages of the exception and all its inner exceptions into one string. |
FindInnerException<T> | Returns the first exception of type T in the chain, or null. |
GetRootCause | Returns the innermost exception, following the first branch of an AggregateException. |
GetAllExceptions | Lists the exception and every inner exception, outermost first. |
Rethrow | Throws a stored exception again and keeps its original stack trace. |
AsDisposable | Turns an Action into an IDisposable that runs it once. |
DisposeAll | Disposes every item in a sequence, even when some of them throw. |
How the chain is walked
GetAllMessages, FindInnerException<T> and GetAllExceptions walk the same tree:
- They start with the exception itself, then follow
InnerException. - At an
AggregateExceptionthey visit every entry ofInnerExceptions, not only the first. - The order is depth-first, outermost first: one branch is walked to the bottom before the next branch starts.
For this exception:
InvalidOperationException first = new InvalidOperationException("a");
TimeoutException deep = new TimeoutException("c");
FormatException second = new FormatException("b", deep);
AggregateException aggregate = new AggregateException(first, second);
Exception exception = new InvalidOperationException("top", aggregate);
the walk order is exception, aggregate, first, second, deep.
GetRootCause works differently. It follows a single path down, and at an AggregateException it takes only the first inner exception.
Reading an exception chain
GetAllMessages
GetAllMessages(string separator = " --> ") joins the message of the exception and of every inner exception, outermost first. Use it for a one-line log entry or an error column in a report, where ex.Message alone often hides the real reason.
Exception failure = new InvalidOperationException(
"Could not save order 1042",
new InvalidCastException("Invalid total", new FormatException("'12,5' is not a number")));
string message = failure.GetAllMessages();
// → "Could not save order 1042 --> Invalid total --> '12,5' is not a number"
string piped = failure.GetAllMessages(" | ");
// → "Could not save order 1042 | Invalid total | '12,5' is not a number"
An AggregateException contributes the messages of all its branches, but not its own message, because that message only repeats the inner ones:
AggregateException aggregate = new AggregateException(
"aggregate message",
new InvalidOperationException("a"),
new FormatException("b", new TimeoutException("c")));
Exception exception = new InvalidOperationException("top", aggregate);
string message = exception.GetAllMessages();
// → "top --> a --> b --> c"
- An exception with no inner exception returns its own message.
- An empty
AggregateExceptionhas no message to contribute, so the result is"". exceptionorseparatorbeingnullthrowsArgumentNullException.
Exception messages can contain user data. See JSON and safe logging for masking helpers before you write them to a log.
FindInnerException<T>
FindInnerException<TException>() returns the first exception of type TException in the chain, or null when there is none. The search includes the exception itself and every branch of an AggregateException. Use it to react to one specific cause, such as a timeout, whatever wraps it.
TimeoutException timeout = new TimeoutException("Payment gateway did not answer in 30 s");
Exception failure = new InvalidOperationException(
"Checkout failed",
new FormatException("Could not read the gateway reply", timeout));
TimeoutException? found = failure.FindInnerException<TimeoutException>();
// → timeout (the same instance)
if (failure.FindInnerException<TimeoutException>() is { } gatewayTimeout)
{
// retry the payment, or show a "try again" message
}
Matching uses the type hierarchy, so asking for a base type finds a derived exception:
ArgumentNullException missingName = new ArgumentNullException("customerName");
Exception failure = new InvalidOperationException("Could not create the user", missingName);
ArgumentException? argumentError = failure.FindInnerException<ArgumentException>();
// → missingName
TimeoutException? timeout = failure.FindInnerException<TimeoutException>();
// → null
- If the exception itself is a
TException, it is returned. - When several exceptions match, the first one in walk order wins.
exceptionbeingnullthrowsArgumentNullException.
GetRootCause
GetRootCause() returns the innermost exception: the one that was thrown first and has no inner exception of its own. Use it when you want the original reason, not the wrappers added on the way up.
FormatException root = new FormatException("'12,5' is not a number");
Exception failure = new InvalidOperationException(
"Could not save order 1042",
new InvalidCastException("Invalid total", root));
Exception cause = failure.GetRootCause();
// → root
string reason = failure.GetRootCause().Message;
// → "'12,5' is not a number"
At an AggregateException it follows the first inner exception and ignores the other branches:
TimeoutException root = new TimeoutException("deep");
AggregateException aggregate = new AggregateException(
new FormatException("first", root),
new InvalidOperationException("second"));
Exception exception = new InvalidOperationException("top", aggregate);
Exception cause = exception.GetRootCause();
// → root
- An exception with no inner exception returns itself.
- An empty
AggregateExceptionreturns itself. exceptionbeingnullthrowsArgumentNullException.
Exception.GetBaseException() also walks to the innermost exception, and when an AggregateException sits in the middle of the chain it follows the first inner exception too. The difference appears when you call it on an AggregateException: that type's GetBaseException() override returns the aggregate itself when it holds more than one inner exception. GetRootCause() always continues into the first inner exception, so it returns a leaf exception every time:
AggregateException aggregate = new AggregateException(
new FormatException("first", new TimeoutException("deep")),
new InvalidOperationException("second"));
Exception baseException = aggregate.GetBaseException();
// → aggregate
Exception cause = aggregate.GetRootCause();
// → the TimeoutException "deep"
When you need every branch, use GetAllExceptions.
GetAllExceptions
GetAllExceptions() returns the exception and every inner exception as an IReadOnlyList<Exception>: outermost first, depth-first, with every branch of an AggregateException. Use it when you need to inspect or log each exception, for example its type or its Data.
FormatException inner = new FormatException("'12,5' is not a number");
InvalidCastException middle = new InvalidCastException("Invalid total", inner);
Exception failure = new InvalidOperationException("Could not save order 1042", middle);
IReadOnlyList<Exception> chain = failure.GetAllExceptions();
// → [failure, middle, inner]
int count = chain.Count;
// → 3
InvalidOperationException first = new InvalidOperationException("a");
TimeoutException deep = new TimeoutException("c");
FormatException second = new FormatException("b", deep);
AggregateException aggregate = new AggregateException(first, second);
Exception exception = new InvalidOperationException("top", aggregate);
IReadOnlyList<Exception> all = exception.GetAllExceptions();
// → [exception, aggregate, first, second, deep]
- The list always starts with the exception itself, so it is never empty.
- Unlike
GetAllMessages, the list includes theAggregateExceptioninstances. exceptionbeingnullthrowsArgumentNullException.
Rethrowing
Rethrow
Rethrow() throws the exception again and keeps its original stack trace. A plain throw failure; would reset the stack trace to the line of that throw. Use it for an exception you caught and stored, and want to throw later, outside its catch block, for example after clean-up has run.
Exception? failure = null;
try
{
importer.ImportInvoices(batch);
}
catch (Exception exception)
{
failure = exception;
}
importer.ReleaseLock();
failure?.Rethrow();
The thrown exception is the same instance that was stored, and its stack trace still contains the frames where it was first thrown. .NET appends the location of the rethrow after a --- End of stack trace from previous location --- line.
- The method is marked
[DoesNotReturn], so nullable analysis treats code afterfailure.Rethrow();as unreachable and reports no null warnings there. It does not change the return-path check: a method that returns a value still needs areturnorthrowafter the call (otherwise CS0161). - On .NET 5 and later it calls
ExceptionDispatchInfo.Throw. The netstandard2.1 build callsExceptionDispatchInfo.Capture(exception).Throw(). The behavior is the same. exceptionbeingnullthrowsArgumentNullException.
Inside a catch block, keep using throw;. Rethrow() is for an exception that has already left its catch block.
Disposables
AsDisposable
AsDisposable() turns an Action into an IDisposable. The action runs on the first Dispose(); later calls do nothing. It is thread-safe: when many threads dispose at the same time, the action still runs once. Use it to put "undo" code in a using block, so that it runs even when the block throws.
C# cannot call a method on a lambda, so assign the lambda to an Action variable first:
OrderImportSettings settings = new OrderImportSettings { ValidateTotals = true };
bool previous = settings.ValidateTotals;
Action restore = () => settings.ValidateTotals = previous;
using (restore.AsDisposable())
{
settings.ValidateTotals = false;
importer.Import(legacyOrders, settings);
}
bool afterwards = settings.ValidateTotals;
// → true
int releases = 0;
Action release = () => releases++;
IDisposable handle = release.AsDisposable();
handle.Dispose();
handle.Dispose();
int count = releases;
// → 1
- The action is cleared before it runs, so if it throws, a later
Dispose()does not run it again. onDisposebeingnullthrowsArgumentNullException.
DisposeAll
DisposeAll() disposes every item of an IEnumerable<IDisposable?>, in sequence order. A Dispose() that throws does not stop the others. When all items are done:
- no failure: the method returns normally;
- one failure: that exception is rethrown with its original stack trace (see
Rethrow); - several failures: an
AggregateExceptionis thrown, and itsInnerExceptionshold every failure in sequence order.
Use it when you own several resources and must release all of them, for example in a Dispose method or a finally block.
IDisposable?[] resources = { reader, transaction, connection };
resources.DisposeAll();
null items are skipped, so you can pass fields that may never have been created:
public sealed class InvoiceExport : IDisposable
{
private StreamWriter? _writer;
private FileStream? _archive;
public void Dispose()
{
new IDisposable?[] { _writer, _archive }.DisposeAll();
}
}
When more than one item fails, catch AggregateException to see each failure:
try
{
new IDisposable?[] { ordersCache, auditLog, paymentClient }.DisposeAll();
}
catch (AggregateException aggregate)
{
int failures = aggregate.InnerExceptions.Count;
// → 2 (when ordersCache and paymentClient both throw)
}
- An empty sequence does nothing.
- The sequence is enumerated once.
- A list or array of reference-type disposables works directly, for example
List<FileStream>, becauseIEnumerable<T>is covariant. A sequence ofstructdisposables does not convert; put the items in anIDisposable?[]first. disposablesbeingnullthrowsArgumentNullException.
Items are disposed in the order you list them. Resources are usually released in reverse order of creation, so list the one you opened last first.
See also
- Tasks: timeouts, retries and other helpers for async work.
- Functions: delegate helpers, which also need a delegate variable instead of a lambda.
- JSON and safe logging: log objects without leaking sensitive data.
- Conventions: null handling and argument validation across the library.