DEV Community

André Slupik
André Slupik

Posted on

Async gotcha: returning Task.FromResult or Task.CompletedTask

So you're implementing this async handler or interface method that doesn't have anything to await, and you write something like

async Task<bool> HandleAsync()
{
     DoSomethingSynchronous();
     return true;
}
Enter fullscreen mode Exit fullscreen mode

But now the C# compiler emits a stern warning: "CS1998: you're using async, so you should be awaiting stuff! find something to await!"* Well, you have nothing to await, but you must still return Task. You remove async and return Task.FromResult(true) instead, as top-rated answers by high-reputation members on Stackoverflow recommend. Job done.

Task<bool> HandleAsync()
{
    DoSomethingSynchronous();
    return Task.FromResult(true);
}
Enter fullscreen mode Exit fullscreen mode

Or is it?

As long as HandleAsync is always called and awaited immediately, all is fine.

var success = await HandleAsync(); // This is fine.
Enter fullscreen mode Exit fullscreen mode

But a dark threat lurks in the shadows. Sometimes a try-catch fails to catch an exception.

var task0 = a.HandleAsync();
var task1 = b.HandleAsync();

try
{
     await Task.WhenAll(new[] { task0, task1 });
}
catch (Exception ex)
{
    // this handler does nothing and the exception escapes! What gives??
}
Enter fullscreen mode Exit fullscreen mode

Because HandleAsync is actually synchronous, any exception propagates as soon as it's called, not when it's awaited. This is surprising to most C# programmers. A synchronous method that returns a Task is not what you expect, especially when it's called SomethingAsync. Surprising, unexpected behavior leads to bugs. Bugs lead to unhappy customers and developers alike.

So what should we do instead?

One option would be to disable warning CS1998, but it may point out cases where a method just shouldn't return a Task in the first place. Probably the best thing would be to mark the function as async and await Task.FromResult:

async Task<bool> HandleAsync()
{
    DoSomethingNotAsync();
    return await Task.FromResult(true);
}
Enter fullscreen mode Exit fullscreen mode

Or if it's a plain Task, awaiting Task.CompletedTask.

async Task HandlerAsync()
{
    DoSomethingNotAsync();
    await Task.CompletedTask;
}
Enter fullscreen mode Exit fullscreen mode

Is this slower? Not much - awaiting a completed task does almost nothing, and the runtime might be able to recognize this common pattern and elide all the overhead - but choosing the async-looking-but-actually-synchronous behavior for hypothetical performance reasons firmly falls in the category "evil premature optimization", in my opinion.

*Actually, it says

warning CS1998: 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.

Discussion (0)