There are various strategies for propagating or swallowing exceptions from the Dispose
method, possibly based on whether an unhanded exception was also thrown from the main logic. The best solution would be to leave the decision up to the caller, depending on their specific requirements. I have implemented a generic extension method that does this, offering:
- the default
using
semantics of propagating Dispose
exceptions
- Marc Gravell's suggestion of always swallowing
Dispose
exceptions
- maxyfc's alternative of only swallowing
Dispose
exceptions when there is an exception from the main logic that would otherwise be lost
- Daniel Chambers's approach of wrapping multiple exceptions into an
AggregateException
- a similar approach that always wraps all exceptions into an
AggregateException
(like Task.Wait
does)
This is my extension method:
/// <summary>
/// Provides extension methods for the <see cref="IDisposable"/> interface.
/// </summary>
public static class DisposableExtensions
{
/// <summary>
/// Executes the specified action delegate using the disposable resource,
/// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method.
/// </summary>
/// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam>
/// <param name="disposable">The disposable resource to use.</param>
/// <param name="action">The action to execute using the disposable resource.</param>
/// <param name="strategy">
/// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method.
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="disposable"/> or <paramref name="action"/> is <see langword="null"/>.</exception>
public static void Using<TDisposable>(this TDisposable disposable, Action<TDisposable> action, DisposeExceptionStrategy strategy)
where TDisposable : IDisposable
{
ArgumentValidate.NotNull(disposable, nameof(disposable));
ArgumentValidate.NotNull(action, nameof(action));
ArgumentValidate.IsEnumDefined(strategy, nameof(strategy));
Exception mainException = null;
try
{
action(disposable);
}
catch (Exception exception)
{
mainException = exception;
throw;
}
finally
{
try
{
disposable.Dispose();
}
catch (Exception disposeException)
{
switch (strategy)
{
case DisposeExceptionStrategy.Propagate:
throw;
case DisposeExceptionStrategy.Swallow:
break; // swallow exception
case DisposeExceptionStrategy.Subjugate:
if (mainException == null)
throw;
break; // otherwise swallow exception
case DisposeExceptionStrategy.AggregateMultiple:
if (mainException != null)
throw new AggregateException(mainException, disposeException);
throw;
case DisposeExceptionStrategy.AggregateAlways:
if (mainException != null)
throw new AggregateException(mainException, disposeException);
throw new AggregateException(disposeException);
}
}
if (mainException != null && strategy == DisposeExceptionStrategy.AggregateAlways)
throw new AggregateException(mainException);
}
}
}
These are the implemented strategies:
/// <summary>
/// Identifies the strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method
/// of an <see cref="IDisposable"/> instance, in conjunction with exceptions thrown by the main logic.
/// </summary>
/// <remarks>
/// This enumeration is intended to be used from the <see cref="DisposableExtensions.Using"/> extension method.
/// </remarks>
public enum DisposeExceptionStrategy
{
/// <summary>
/// Propagates any exceptions thrown by the <see cref="IDisposable.Dispose"/> method.
/// If another exception was already thrown by the main logic, it will be hidden and lost.
/// This behaviour is consistent with the standard semantics of the <see langword="using"/> keyword.
/// </summary>
/// <remarks>
/// <para>
/// According to Section 8.10 of the C# Language Specification (version 5.0):
/// </para>
/// <blockquote>
/// If an exception is thrown during execution of a <see langword="finally"/> block,
/// and is not caught within the same <see langword="finally"/> block,
/// the exception is propagated to the next enclosing <see langword="try"/> statement.
/// If another exception was in the process of being propagated, that exception is lost.
/// </blockquote>
/// </remarks>
Propagate,
/// <summary>
/// Always swallows any exceptions thrown by the <see cref="IDisposable.Dispose"/> method,
/// regardless of whether another exception was already thrown by the main logic or not.
/// </summary>
/// <remarks>
/// This strategy is presented by Marc Gravell in
/// <see href="http://blog.marcgravell.com/2008/11/dontdontuse-using.html">don't(don't(use using))</see>.
/// </remarks>
Swallow,
/// <summary>
/// Swallows any exceptions thrown by the <see cref="IDisposable.Dispose"/> method
/// if and only if another exception was already thrown by the main logic.
/// </summary>
/// <remarks>
/// This strategy is suggested in the first example of the Stack Overflow question
/// <see href="https://mcmap.net/q/95036/-swallowing-exception-thrown-in-catch-finally-block/1149773">Swallowing exception thrown in catch/finally block</see>.
/// </remarks>
Subjugate,
/// <summary>
/// Wraps multiple exceptions, when thrown by both the main logic and the <see cref="IDisposable.Dispose"/> method,
/// into an <see cref="AggregateException"/>. If just one exception occurred (in either of the two),
/// the original exception is propagated.
/// </summary>
/// <remarks>
/// This strategy is implemented by Daniel Chambers in
/// <see href="http://www.digitallycreated.net/Blog/51/c%23-using-blocks-can-swallow-exceptions">C# Using Blocks can Swallow Exceptions</see>
/// </remarks>
AggregateMultiple,
/// <summary>
/// Always wraps any exceptions thrown by the main logic and/or the <see cref="IDisposable.Dispose"/> method
/// into an <see cref="AggregateException"/>, even if just one exception occurred.
/// </summary>
/// <remarks>
/// This strategy is similar to behaviour of the <see cref="Task.Wait()"/> method of the <see cref="Task"/> class
/// and the <see cref="Task{TResult}.Result"/> property of the <see cref="Task{TResult}"/> class:
/// <blockquote>
/// Even if only one exception is thrown, it is still wrapped in an <see cref="AggregateException"/> exception.
/// </blockquote>
/// </remarks>
AggregateAlways,
}
Sample use:
new FileStream(Path.GetTempFileName(), FileMode.Create)
.Using(strategy: DisposeExceptionStrategy.Subjugate, action: fileStream =>
{
// Access fileStream here
fileStream.WriteByte(42);
throw new InvalidOperationException();
});
// Any Dispose() exceptions will be swallowed due to the above InvalidOperationException
Update: If you need to support delegates that return values and/or are asynchronous, then you could use these overloads:
/// <summary>
/// Provides extension methods for the <see cref="IDisposable"/> interface.
/// </summary>
public static class DisposableExtensions
{
/// <summary>
/// Executes the specified action delegate using the disposable resource,
/// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method.
/// </summary>
/// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam>
/// <param name="disposable">The disposable resource to use.</param>
/// <param name="strategy">
/// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method.
/// </param>
/// <param name="action">The action delegate to execute using the disposable resource.</param>
public static void Using<TDisposable>(this TDisposable disposable, DisposeExceptionStrategy strategy, Action<TDisposable> action)
where TDisposable : IDisposable
{
ArgumentValidate.NotNull(disposable, nameof(disposable));
ArgumentValidate.NotNull(action, nameof(action));
ArgumentValidate.IsEnumDefined(strategy, nameof(strategy));
disposable.Using(strategy, disposableInner =>
{
action(disposableInner);
return true; // dummy return value
});
}
/// <summary>
/// Executes the specified function delegate using the disposable resource,
/// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method.
/// </summary>
/// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam>
/// <typeparam name="TResult">The type of the return value of the function delegate.</typeparam>
/// <param name="disposable">The disposable resource to use.</param>
/// <param name="strategy">
/// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method.
/// </param>
/// <param name="func">The function delegate to execute using the disposable resource.</param>
/// <returns>The return value of the function delegate.</returns>
public static TResult Using<TDisposable, TResult>(this TDisposable disposable, DisposeExceptionStrategy strategy, Func<TDisposable, TResult> func)
where TDisposable : IDisposable
{
ArgumentValidate.NotNull(disposable, nameof(disposable));
ArgumentValidate.NotNull(func, nameof(func));
ArgumentValidate.IsEnumDefined(strategy, nameof(strategy));
#pragma warning disable 1998
var dummyTask = disposable.UsingAsync(strategy, async (disposableInner) => func(disposableInner));
#pragma warning restore 1998
return dummyTask.GetAwaiter().GetResult();
}
/// <summary>
/// Executes the specified asynchronous delegate using the disposable resource,
/// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method.
/// </summary>
/// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam>
/// <param name="disposable">The disposable resource to use.</param>
/// <param name="strategy">
/// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method.
/// </param>
/// <param name="asyncFunc">The asynchronous delegate to execute using the disposable resource.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public static Task UsingAsync<TDisposable>(this TDisposable disposable, DisposeExceptionStrategy strategy, Func<TDisposable, Task> asyncFunc)
where TDisposable : IDisposable
{
ArgumentValidate.NotNull(disposable, nameof(disposable));
ArgumentValidate.NotNull(asyncFunc, nameof(asyncFunc));
ArgumentValidate.IsEnumDefined(strategy, nameof(strategy));
return disposable.UsingAsync(strategy, async (disposableInner) =>
{
await asyncFunc(disposableInner);
return true; // dummy return value
});
}
/// <summary>
/// Executes the specified asynchronous function delegate using the disposable resource,
/// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method.
/// </summary>
/// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam>
/// <typeparam name="TResult">The type of the return value of the asynchronous function delegate.</typeparam>
/// <param name="disposable">The disposable resource to use.</param>
/// <param name="strategy">
/// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method.
/// </param>
/// <param name="asyncFunc">The asynchronous function delegate to execute using the disposable resource.</param>
/// <returns>
/// A task that represents the asynchronous operation.
/// The task result contains the return value of the asynchronous function delegate.
/// </returns>
public static async Task<TResult> UsingAsync<TDisposable, TResult>(this TDisposable disposable, DisposeExceptionStrategy strategy, Func<TDisposable, Task<TResult>> asyncFunc)
where TDisposable : IDisposable
{
ArgumentValidate.NotNull(disposable, nameof(disposable));
ArgumentValidate.NotNull(asyncFunc, nameof(asyncFunc));
ArgumentValidate.IsEnumDefined(strategy, nameof(strategy));
Exception mainException = null;
try
{
return await asyncFunc(disposable);
}
catch (Exception exception)
{
mainException = exception;
throw;
}
finally
{
try
{
disposable.Dispose();
}
catch (Exception disposeException)
{
switch (strategy)
{
case DisposeExceptionStrategy.Propagate:
throw;
case DisposeExceptionStrategy.Swallow:
break; // swallow exception
case DisposeExceptionStrategy.Subjugate:
if (mainException == null)
throw;
break; // otherwise swallow exception
case DisposeExceptionStrategy.AggregateMultiple:
if (mainException != null)
throw new AggregateException(mainException, disposeException);
throw;
case DisposeExceptionStrategy.AggregateAlways:
if (mainException != null)
throw new AggregateException(mainException, disposeException);
throw new AggregateException(disposeException);
}
}
if (mainException != null && strategy == DisposeExceptionStrategy.AggregateAlways)
throw new AggregateException(mainException);
}
}
}