What effect does including async in signature (without await in body) have on program flow?

General question

I've just tracked down a strange behaviour to the the absence of an async modifier in the signature of a method. With it, I get the expected behaviour, but without it, my program behaves oddly.

The thing is, the body of the method is entirely synchronous: not a single await in sight. I even (as expected) get compiler warning 1998:

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

The temptation then is to remove it. (I mean, the compiler is basically telling me to!) When I do remove it, I start reliably getting unexpected behaviour.

My question is: what is async really doing when there isn't an await?


Context / specifics

I don't think the specifics of my application will be relevant to the underlying explanation that I'm keen to understand, but I'll include it here to give some flavour...

The program I'm developing consumes messages from a service bus. The signature of my method is:

public async Task<BrokeredMessage> ProcessMessageAsync(BrokeredMessage inMessage) 

Historical context: I initially borrowed this from a similar program written by a colleague and the method name (which implies async operation) is part of an implementation of an interface.

The odd behaviour I'm experiencing when I remove the async modifier is that the body of the method is executed, but the message is returned as undeliverable. An interesting consequence of this is that I end up with the message being processed for each retry attempt. (I'm just lucky this was a low number!)


Update

As pointed out by Mr Skeet, it is significant that, although the signature promises a return value of type Task, my method actually returns null.

Jon Skeet
people
quotationmark

My question is: what is async really doing when there isn't an await?

In terms of observable behaviour:

  • The code will still run synchronously, as the warning says
  • The result (including any exception thrown) will be wrapped up in a Task

In terms of implementation, the compiler will generate a state machine to handle all the async-ness. That shouldn't change any other behaviour though.

I can't immediately think of any reason you'd see this message behaviour, unless it's a difference due to the handling of exceptions. If the calling code is catching an exception but ignoring a faulted task (or vice versa) that could definitely explain a difference. My first diagnostic step would be to add some logging to see whether the method does complete successfully or whether an exception is thrown somewhere.

Now you've stated that you're returning null, that's a definite difference from the caller's perspective: there's a huge difference between a method returning a null value for Task<BrokeredMessage>, and a method returning a Task<BrokeredMessage> with a null value for its Result property. Perhaps the caller is dereferencing the returned value, and a NullReferenceException is being thrown?

If that is the case, your first port of call should be to work out why you haven't been able to see that exception before - it suggests there's some logging missing. You can then fix it by changing this:

return null;

to

return Task.FromResult<BrokeredMesage>(null);

people

See more on this question at Stackoverflow