Async-Async: Consequences for exceptions

Raymond Chen

Raymond

As we’ve been learning, the feature known as Async-Async makes asynchronous operations even more asynchronous by pretending that they started before they actually did. The effect of Async-Async is transparent to properly-written applications, but if you have been breaking the rules, you may notice some changes to behavior. Today we’ll look at exceptions.

// Code in italics is wrong.

Task task1 = null;
Task task2 = null;
try
{
    task1 = DoSomethingAsync(arg1);
    task2 = DoSomethingAsync(arg2);
}
catch (ArgumentException ex)
{
    // One of the arguments was invalid.
    return;
}

// Wait for the operations to complete.
await Task.WhenAll(task1, task2);

This code “knows” that the invalid parameter exception is raised as part of initiating the asynchronous operation, so it catches the exception only at that point.

With Async-Async, the call to Do­Something­Async returns a fake IAsync­Operation immediately, before sending the call to the server. If the server returns an error in response to the operation, it’s too late to report that error to the client as the return value of Do­Something­Async. Because, y’know, time machine.

The exception is instead reported to the completion handler for the IAsync­Operation. In the above case, it means that the exception is reported when you await the task, rather than when you create the task.

try
{
    Task task1 = DoSomethingAsync(arg1);
    Task task2 = DoSomethingAsync(arg2);

    // Wait for the operations to complete.
    await Task.WhenAll(task1, task2);
}
catch (ArgumentException ex)
{
    // One of the arguments was invalid.
    return;
}

Again, this is not something that should affect a properly-written program, because you don’t know when the server is going to do its parameter validation. It might do parameter validation before creating the IAsync­Operation, or it might defer doing the parameter validation until later for performance reasons. You need to be prepared for the exception to be generated at either point.

In practice, this is unlikely to be something people stumble across because Windows Runtime objects generally reserve exceptions for fatal errors, so you have no need to try to catch them.

Raymond Chen
Raymond Chen

Follow Raymond   

6 Comments
Henke37
Henrik Andersson 2019-05-03 08:47:22
Of course, in non toy programs, you shouldn't be catching ArgumentException. That exception is only for when you've made a mistake and not a normal exception.
Avatar
Ihor Nechyporuk 2019-05-03 12:04:55
But won't ArgumentException be wrapped in AggregateException if it's thrown at await Task.WhenAll(task1, task2)? So basically catch block won't help.
Avatar
Ben Voigt 2019-05-07 07:16:28
Two successive days you've claimed that "proper" calling code receiving a System.Tasks.Task (or local equivalent) cannot know anything about the demarcation between synchronous and asynchronous stages of the call.  But I disagree.  If you were, for example, to store a Task into a collection and retrieve it later, the retrieval, although it certainly does return a Task object, is completely and reliably synchronous just as a retrieval operation from a collection containing primitives. Is there any documentation describing the rules for when the whole call must be assumed to be asynchronous?