A word of caution on argument validation in Async methods

I have been noticing around in some blogposts and code snippets a pattern being used when validating arguments in Task-returning async methods and that is returning the ArgumentException as part of the Task
instead of immediately failing (directly raising the exception). Something didn’t feel right so i thought i’d try it

Let’s start with defining these 2 async method variations for argument validation :

The first one assigns the exception to the returned Task and the second one immediately raises it the good old-fashioned way.

Next we write our tests that cover the usage scenarios of our async methods :

Both tests try to call our async methods with a null argument by both awaiting and not awaiting them (fire and forget).
And here is what we get after running it :

As expected, we notice that the thrown ,argument validating, exception when assigned to the returning Task and when not awaited by the caller is completely lost! Like it never happened. The Task “swallowed” it.

In most cases this is the expected behavior, when the exception is part of your Task logic and isn’t awaited, but these kind of exceptions should not go unnoticed! They represent usage errors and should be communicated to the caller fast and always. These can be your usual pre-conditions like argument validation, state validation etc.

One of the lessons from this is that you should also be aware of the implications of your async method not being awaited.

I highly recommend reading the “Task-Based Asynchronous Pattern” document from Microsoft here where
it states :


An asynchronous method should only directly raise an exception to be thrown out of the MethodNameAsync call in response to a usage error*. For all other errors, exceptions occurring during the execution of an asynchronous method should be assigned to the returned Task. This is the case even if the asynchronous method happens to complete synchronously before the Task is returned. Typically, a Task will contain at most one exception.  However, for cases where a Task is used to represent multiple operations (e.g. Task.WhenAll), multiple exceptions may be associated with a single Task.

(*Per .NET design guidelines, a usage error is something that can be avoided by changing the code that calls the method. For example, if an error state results when a null is passed as one of the method’s arguments, an error condition usually represented by an ArgumentNullException, the calling code can be modified by the developer to ensure that null is never passed. In other words, the developer can and should ensure that usage errors never occur in production code.)