Anduril
Anduril

Reputation: 1276

MVC function returns but waits for async function before sending result

I was trying to explain to a colleague why async void functions are bad and that exceptions won't get caught, but it turns out I might not be understanding them right. We have a piece of code that looks a bit like this:

public ActionResult EmailCandidates(List<string> identityTokens, string subject, string content)
{
    // generate list of recipients here
    SendEmail(recipients, subject, content); //not awaited
    return new AjaxResponse { // AjaxResponse is a wrapper around JSONResponse
        IsSuccess = true,
        Data = recipients.Select(r=>r.Name)
    };
}

private async void SendEmail(List<EmailAddress> recipients, string subject, string content)
{
    await Task.Delay(10000); // simulate async send email
    throw new Exception(); // manually added
}

What I was expecting, and what I was trying to explain is that if the SendEmail function throws an exception it won't be caught properly because the main function EmailCandidates has already returned to the client. Only that's not what happens. The code above executes in exactly the order I expect:

and then it gets kind of weird:

So why even though EmailCandidates has returned, does the response not get sent to the client. How does is know to wait for the async SendEmail function?

Upvotes: 1

Views: 924

Answers (3)

fatherOfWine
fatherOfWine

Reputation: 1251

Async void methods are kinda different beasts from the "normal" async methods.They have different error handling logic. When an exception is thrown out of an async Task or async Task<T> method, that exception is captured and placed on the Task object. With async void methods, there is no Task object, so any exceptions thrown by an async void method will be raised directly on the SynchronizationContext that was active when the async void method has been called. These exceptions can be observed using UnhandledException event handler.

Upvotes: 0

Zinov
Zinov

Reputation: 4119

Your application is working fine, actually is following the default behavior that MVC has. If you put explicitly the exception, then this kind of errors(500) arise when the request is originated from the same machine where the application is on(localhost) if you want to see what the actual user want to see you need to change the value on the webconfig for the By default it is set to RemoteOnly.

If you go to your FilterConfig. You will see this line of code

 public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
        }

Try to change the value to "On" and you will see that you ended on that error page because the handler error attribute, it is providing post processing logic on an action and when it sees that an exception has escaped from an action it will display an error view instead of the yellow screen of death. The error View is on your application by default inside View/Shared/Error.cshtml

Your Off option

Specifies that custom errors are disabled. This allows display of detailed errors.

for reference go here: https://msdn.microsoft.com/en-us/library/h0hfz6fc(v=vs.71).aspx

If you put remote only then you will continue seeing the error if you are debugging the website in your localmachine, but if you host the application the final user will not see that error.

Upvotes: 0

Stephen Cleary
Stephen Cleary

Reputation: 456417

ASP.NET provides a SynchronizationContext that does keep track of the number of asynchronous operations in flight, and will not send a result until they have all completed. Note that this SynchronizationContext has been removed in ASP.NET Core.

However, you shouldn't be seeing this behavior even on ASP.NET. In the case of a synchronous method calling an async void method, you should see an InvalidOperationException with the message "An asynchronous operation cannot be started at this time.". In the case of an asynchronous method calling an async void method (that doesn't complete before the handler returns), you should see an InvalidOperationException with the message "An asynchronous module or handler completed while an asynchronous operation was still pending."

Since neither of these safety nets are triggering, I suspect that your ASP.NET code is using an old version of ASP.NET. Those safety nets were added in .NET 4.5, which you have to not only have as a build target but you also have to add targetFramework in your web.config.

A new .NET 4.5.2 ASP.NET MVC app with the following code immediately throws an InvalidOperationException, as expected:

public ActionResult About()
{
    ViewBag.Message = "Your application description page.";
    Test();
    return View();
}

private async void Test()
{
    await Task.Delay(20000);
    throw new Exception("Blah");
}

Upvotes: 4

Related Questions