2

I have a long chain of calls that eventually calls an asynchronous function in another assembly. I want this function to be executed synchronously, and it may throw an exception, which I want to propagate up the call chain.

This scenario is minimally reproduced by the following snippet:

static Task<int> Exc()
{
    throw new ArgumentException("Exc");
    return Task.FromResult(1);
}

static async void DoWork()
{
    await Exc();
}

static void Main(string[] args)
{
    try
    {
        DoWork();
    }
    catch (Exception e)
    {
        Console.WriteLine("Caught {0}", e.Message);
    }
}

This code will cause a crash, because the exception thrown from Exc doesn't get propagated back to Main.

Is there any way for me to have the exception thrown from Exc be handled by the catch block in Main, without changing every function in my call chain to use async, await, and return a Task?

In my actual code, this asynchronous function is called from a very deep chain of function calls, all of which should executed synchronously. Making them all asynchronous when they'll never be used (nor can they be safely used) asynchronously seems like a terrible idea, and I'd like to avoid it, if possible.

Collin Dauphinee
  • 12,840
  • 1
  • 36
  • 59
  • You can install a custom synchronization context because the error will be posted to it. I think Nito Async has one. – usr Dec 15 '15 at 20:29
  • @usr this won't let the exception be *"handled by the catch block in Main"* though. – i3arnon Dec 15 '15 at 20:33
  • @i3arnon the sync context can wait until the async void method is done. The exception will be present then. Don't async void methods even use the "operations outstanding" counter on a sync context? Not sure. Update: Yes, they do. – usr Dec 15 '15 at 20:39
  • @usr it will be present, but it can't be really propagated into `main` as it behaves in a synchronous method. To just get the hands on the exception and do something with it you can just register a continuation on the task. – i3arnon Dec 15 '15 at 20:42
  • Here, Main could be modified to capture the exception using said technique. He should be able to inspect it then. – usr Dec 15 '15 at 20:44
  • 2
    What modifications are you willing to make? Are you willing to modify Main? Modify DoWork? Would `void DoWork() { Exec().Wait(); } ` work for you? – usr Dec 15 '15 at 20:44
  • @Default: Yes, sorry. I mean back to `Main`, I'll update that. – Collin Dauphinee Dec 15 '15 at 20:47
  • You can try this http://stackoverflow.com/questions/33082100/executing-async-function-synchronously/33085064#33085064. Nobody responded either positive or negative, and in my own test it worked – Ivan Stoev Dec 15 '15 at 20:48
  • @usr: I'm willing to modify anything, but my problem is that the actual code has the asynchronous function 16 calls deep. I don't want to modify all 16 calls (+ anything invoking them). `Exec().Wait()` is not possible, as it may deadlock. – Collin Dauphinee Dec 15 '15 at 20:48

3 Answers3

3

According to MSDN, no.

You use the void return type primarily to define event handlers, which require that return type. The caller of a void-returning async method can't await it and can't catch exceptions that the method throws.

Bradford Dillon
  • 1,610
  • 10
  • 23
  • In my actual code, there are 16 function calls between the `catch` block I want to handle it and the asynchronous function. At this point, `async`/`await` has basically become cancer to my code. – Collin Dauphinee Dec 15 '15 at 20:31
  • Could you run the `async` function synchronously at the bottom of the call chain and rethrow any exceptions raised by the `async` operation? – Bradford Dillon Dec 15 '15 at 20:33
  • Unless I'm missing something, the only way I can run it synchronously from a non-async function is by doing `Exc().Wait()`, which may cause a deadlock. – Collin Dauphinee Dec 15 '15 at 20:35
3

This code will cause a crash, because the exception thrown from Exc doesn't get propagated back to DoWork.

Not at all. The exception from Exc is being passed to DoWork and can be caught there if you have a try/catch in DoWork.

The reason you're seeing a crash is because DoWork is propagating that exception, and DoWork is an async void method. This can be easily avoided by making DoWork an async Task method instead (note that async void should only be used for event handlers, which DoWork is clearly not). As I describe in my MSDN article on best practices, strive to avoid async void.

In my actual code, this asynchronous function is called from a very deep chain of function calls, all of which should executed synchronously. Making them all asynchronous when they'll never be used (nor can they be safely used) asynchronously seems like a terrible idea, and I'd like to avoid it, if possible.

The operation is either asynchronous or it is is not. Since the low-level API you're calling is asynchronous, then it's best if your code consumes it asynchronously. Trying to wrap asynchronous code in synchronous code is extremely error-prone and a terrible idea. Though, sometimes, it is necessary.

So, the cleanest solution is to make asynchronous methods have asynchronous signatures. And yes, that means going "async all the way", as I describe in my MSDN article on async best practices. However, if you prefer to do sync-over-async, then you can choose one of a variety of hacks that I describe in my recent "Brownfield async" article.

Stephen Cleary
  • 376,315
  • 69
  • 600
  • 728
0

I will provide a counter point to Stephens answer in case you insist on not making the entire stack async:

Would void DoWork() { Exec().Wait(); } work for you?

There are no async deadlocks in a console app. If this is not a console app you can use what Ivan linked to, or use ConfigureAwait(false), or use Task.Run(...).Wait() which does not deadlock because the task body has no sync context.

Note, that by doing any of that any potential gain from async IO is lost.

Community
  • 1
  • 1
usr
  • 162,013
  • 33
  • 219
  • 345
  • Won't either of these approaches still deadlock if the entire thread pool is blocked on `Wait()` calls? – Collin Dauphinee Dec 15 '15 at 20:57
  • Yes, any approach not being fully async has that problem. There is no workaround for that, that is the *point* of async IO. That problem is very rare though. Do you plan on having 100s of these computations running at the same time? – usr Dec 15 '15 at 20:58
  • Yes; the asynchronous function actually takes anywhere from 5-10 seconds to complete, and the entry point to my call chain is from a service host (one consumed internally, by developers). I have a very specific case where I need to wait, but normally consumers wouldn't want to. – Collin Dauphinee Dec 15 '15 at 21:00
  • Are you aware that async does not make the IO go any faster at all? Just in case I'll link you my standard material to understand when to use async and when not: http://stackoverflow.com/a/25087273/122718 Why does the EF 6 tutorial use asychronous calls? http://stackoverflow.com/a/12796711/122718 Should we switch to use async I/O by default? – usr Dec 15 '15 at 22:39