C# Async await best practices

Don’t use async void

An async method may return Task, Task<T> and void. Natural return types are all but void.

Any sync method returning void becomes an async method returning Task. example:

// synchronous work
void MyMethod()
{
	Thread.sleep(1000);
}

// asynchronous work
async Task MyAsyncMethod()
{
	await Task.Delay(1000);
}

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. For async void methods, exceptions aren’t caught.

example:

// DON'T DO THIS - exception not thrown
private async void ThrowExceptionAsync()
{
	throw new InvalidOperationException();
}

public void AsyncVoidExceptions_CannotBeCaught()
{
	try
	{
		ThrowExceptionAsync();
	}
	catch(Exception)
	{
		// The exception is never caught here!
		throw;
	}
}

The exception to use async void are asynchronous event handlers.

example:

private async void button1_clicl(object sender, EventArgs e)
{
	await Button1ClickAsync(); // my method
}

Prefer Task.FromResult over Task.Run for trivially computed data

For pre-computed results, there’s no need to call Task.Run which will queue a work item to the thread pool that will immediately complete with the pre-computed value. Instead, use Task.FromResult to create a task which wraps already computed data.

public Task<int> AddSync(int a, int b)
{
	return Task.FromResult(a+b);
}

Don’t use Task.Run for work that blocks the thread for a long time

Task.Run is not meant for work that blocks the thread for the lifetime of the application doing background work. Task.Run queues a thread from the pool, with the assumption that this work will finish quickly.

// DON'T DO THIS - blocks a thread for lifetime
public class QueueProcessor
{
	//..
	public void StartProcessing()
	{
		Task.Run(ProcessQueueForLifetime);
	}
	//..
}

Instead, Task.Factory.StartNew has an option TaskCreationOptions.LongRunning

public class QueueProcessor
{
	//..
	public Task StartProcessing() => Task.Factory.StartNew(ProcessQueueForLifetime, TaskCreationOptions.LongRunning);
	//..
}

this also has the advantage that it can be easily combined with await and other methods such as Task.WhenAll

Avoid using Task.Result and Task.Wait

There are few ways to correctly use Task.Result and Task.Wait, so the general advice is to completely avoid them.
Using Task.Result or Task.Wait to block waiting on an asynchronous call is much worse than calling a truly synchronous API to block.

Reference(s)

https://docs.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming
https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md