Async-Async: Consequences for exceptions

Raymond Chen

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.

6 comments

Discussion is closed. Login to edit/delete existing comments.

  • Henrik Andersson 0

    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.

    • cheong00 0

      Unless you’re on web, or feeding parameters from non-typed configuration file (say, .INI)

  • Ihor Nechyporuk 0

    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.

    • Raymond ChenMicrosoft employee 0

      Oh, yeah, you’ll have to tweak the exception handling, too.

  • Ben Voigt 0

    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?

    • Raymond ChenMicrosoft employee 0

      Sure, you can synchronously retrieve the Task from a collection in which you saved it. But we’re not talking about the reference to the Task. We’re talking about the code that running inside the Task. All you know is whether the code has produced a result. You don’t know how much progress it has made toward starting.

Feedback usabilla icon