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

Comments are closed.

  • Henrik Andersson
    Henrik Andersson

    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
      cheong00

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

  • Avatar
    Ihor Nechyporuk

    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

    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 Chen
      Raymond Chen

      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.