Updated on
Task.CompletedTask, Task.FromResult() and a plain return all answer one question: how does a method hand back a Task when there is nothing to wait for?
Return Task.CompletedTask when the method’s return type is Task and the work is already done. Return Task.FromResult(value) when the return type is Task<T> and we already hold the value. Write a plain return value inside an async method, where the compiler builds the task for us and return supplies only the result.
One rule falls out of those three, and it is the one the rest of this article keeps coming back to: a method that awaits nothing should not be marked async at all.
Let’s start.
When Should We Use Task.CompletedTask in a C# Async Method?
Use Task.CompletedTask when a method must return a Task and has no asynchronous work to do.
It is the already-finished task. Microsoft Learn defines the property as one that “Gets a task that has already completed successfully”, and its Status is RanToCompletion before we do anything with it.
The method that returns it needs no async keyword, and that is the point. Without async the compiler builds no state machine, so handing back Task.CompletedTask from an ordinary method is the cheapest way to satisfy a Task-returning signature.
That signature is usually not ours to change. An interface member, a BackgroundService override, a middleware delegate or an event handler all demand a Task back because they follow the asynchronous programming pattern, and plenty of real implementations have nothing to wait for.
Awaiting it is the same as returning it, only slower. await Task.CompletedTask resumes immediately because the task is already complete, so the await buys nothing that returning the task does not already give the caller.
The property’s own remarks carry the one caveat worth printing: “Repeated attempts to retrieve this property value may not always return the same instance.”
Let’s create the TaskCompletedHandler class to understand this:
public class TaskCompletedHandler
{
public Task UseTaskCompletedAsync()
{
Console.WriteLine("Not performing any asynchronous work.");
return Task.CompletedTask;
}
}
By using Task.CompletedTask in the UseTaskCompletedAsync() method, we avoid unnecessary overhead and resource consumption, as we’re only writing to the console and returning an already completed task without additional processing.
Those imposed signatures come from the asynchronous programming pattern, which is why so many of them hand back a Task with nothing to await inside.
The imposed-signature case looks the same, and it is the one we meet most often. An interface says a handler returns a Task, and this handler only writes a line:
public class ConsoleNotificationHandler : INotificationHandler
{
public Task HandleAsync(string message)
{
Console.WriteLine(message);
return Task.CompletedTask;
}
}
The interface demands a Task, the implementation has nothing to await, and Task.CompletedTask satisfies the contract without an async keyword anywhere in the class.
When a task has not finished, Wait() and .Result really do block the calling thread, which is the difference between await and Task.Wait.
When Should We Use Task.FromResult in a C# Async Method?
Use Task.FromResult() when a method returns Task<T> and already holds the value.
It is Task.CompletedTask with a payload. Microsoft Learn says the method “Creates a Task<TResult> that’s completed successfully with the specified result” and is “commonly used when the return value of a task is immediately known without executing a longer code path”.
The method that calls it should not be async. Task.FromResult() produces the finished task itself, so async wraps a state machine around work that is already over.
return await Task.FromResult(value) is the line to unlearn. Inside an async method, return value produces the same task through the machinery the async keyword already built, and it does it once instead of twice.
A cache sits behind the method for some values. Microsoft Learn notes that since .NET 6 it “may return a cached singleton object rather than allocating a new object” for some result types and values, so the cost is not always an allocation.
Two siblings cover the other endings: Task.FromException() and Task.FromCanceled().
Both quotations above come from the method’s documentation. Let’s create the UseTaskFromResultAsync() method to understand this:
public class TaskFromResultHandler
{
public Task<string> UseTaskFromResultAsync()
{
Console.WriteLine("Not performing any asynchronous work but returning a result.");
var message = "Hello, world!";
return Task.FromResult(message);
}
}
Using Task.FromResult in the UseTaskFromResultAsync() method allows us to return a string result with the Task. Additionally, we use Task<string> in our method signature rather than Task.
Do not write it this way, even though it compiles and returns the right string:
public async Task<string> UseTaskFromResultAsync()
{
Console.WriteLine("Not performing any asynchronous work but returning a result.");
var message = "Hello, world!";
return await Task.FromResult(message);
}
The async keyword builds a state machine to await a task that is already complete. Dropping async and await and returning Task.FromResult(message) directly gives the caller the same finished task for half the allocation.
If the value is already in hand there is no task to unwrap; the genuinely hard case is running an async method synchronously, where the task is real and the caller cannot await.
When Should We Use a return Statement in an Async Method?
Use a plain return when the method genuinely awaits something.
Inside an async method the compiler builds the task. We return the value, an int or a string or nothing at all, and the generated state machine wraps it in the Task<T> the signature promises. Calling Task.FromResult() there is the same work done a second time.
await is what suspends the method, not return. Execution stops at each await until the awaited operation finishes, and the return at the end runs on whichever thread resumed the method.
The keyword also moves where exceptions surface. An ordinary method that throws before it produces a task throws at the call site, before the caller awaits anything. An async method captures the exception inside the task it returns, so the caller sees it only on the await.
That difference is the strongest argument for async once there is real awaiting to do, and the strongest argument against it when there is not.
Let’s create the UseReturnAsync() method and see how we can achieve this using a return statement:
public class ReturnHandler
{
public async Task<int> UseReturnAsync()
{
Console.WriteLine("About to perform some asynchronous work.");
await Task.Delay(10);
return 20;
}
}
In the UseReturnAsync() method, we write a message to the console, carry out an asynchronous operation, and then return a task representing the completion of that operation. The caller can await this task to wait for the result.
The method pauses at each await, not at the return. By the time the return runs, the awaited operation has finished and the state machine only has to publish the value.
The return keyword is used in a similar way in both synchronous and asynchronous methods to provide a value as the result of the method. Whether the method is synchronous or asynchronous, the basic purpose of the return keyword remains the same: to send a value back from the method to the calling code.
How Do We Choose Between Task.CompletedTask, Task.FromResult and return?
Three questions settle it, in this order.
Does the method await anything? If it does, mark it async, write a plain return, and reach for neither factory method.
Does it return a value? A Task with nothing to wait for gets Task.CompletedTask. A Task<T> whose value is already in hand gets Task.FromResult(value).
Is the async keyword still there? If the body contains no await, take it off. The factory methods hand back a finished task on their own, and async adds a state machine with nothing to drive.
Failure and cancellation have their own factories. Task.FromException() and Task.FromCanceled() return a faulted or canceled task the caller can await, instead of throwing before the task exists.
On a hot path where the answer is usually already known, ValueTask and ValueTask<T> skip the allocation, at the cost of rules about awaiting them only once.
Handing back a task somebody else produced is a different question again, and the answer is the difference between returning a task and awaiting it. Before reaching for the allocation-free option, it is worth reading about ValueTask and when it is worth the extra rules, because a value type that may only be awaited once is not a drop-in replacement for a Task.
| The method... | Return type | What to write | async keyword |
|---|---|---|---|
| has nothing to await and no value to give back | Task | return Task.CompletedTask; | No |
| has nothing to await and already holds the value | Task<T> | return Task.FromResult(value); | No |
| has nothing to await and has already failed | Task | return Task.FromException(ex); | No |
| has nothing to await, already failed, returns a value | Task<T> | return Task.FromException<T>(ex); | No |
| was canceled before it started | Task / Task<T> | return Task.FromCanceled(token); / Task.FromCanceled<T>(token) | No |
| really awaits something and gives back no value | Task | await ...; with no return value | Yes |
| really awaits something and gives back a value | Task<T> | await ...; return value; | Yes |
| usually has the answer already and runs hot | ValueTask / ValueTask<T> | return ValueTask.FromResult(value); | No |
Conclusion
Task.CompletedTask and Task.FromResult() both hand back a task that is already finished, one without a value and one with. Neither needs the async keyword, and adding it costs a state machine that has nothing to run.
A plain return belongs inside an async method that really awaits something. There the compiler builds the task and return only supplies the value.
The question to ask of any method returning a Task is whether it awaits anything. If it does not, one of the factory methods is the answer and async comes off the signature.
Tested with .NET 10.0.10.
