Different implementations of a method that returns a Task
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.
Introduction
In C#, a method that returns Task or Task<T> can be implemented in several ways: with async/await, by returning Task.CompletedTask for synchronous work, by returning Task.FromResult<T> for known values, or by wrapping synchronous code in Task.Run. Each approach has different performance characteristics and use cases. Choosing the right one depends on whether the method does actual asynchronous work, synchronous work, or a mix of both.
async/await (Standard Async Method)
The most common approach when the method performs actual asynchronous operations:
The compiler generates a state machine that manages the asynchronous flow. Use this when the method contains at least one await.
Task.CompletedTask (Synchronous, No Return Value)
When a method implements an interface requiring Task but does no async work:
Task.CompletedTask is a cached singleton — no allocation occurs. This is better than async + no await because it avoids the state machine overhead.
Task.FromResult (Synchronous, With Return Value)
When a method returns Task<T> but computes the value synchronously:
Task.FromResult creates a completed Task<T> with the given value. For common values like true, false, 0, and null, the runtime caches the task objects.
Task.Run (Offload to Thread Pool)
Wraps CPU-bound synchronous work in a task that runs on the thread pool:
Use Task.Run only when you need to move CPU-bound work off the calling thread (e.g., to keep the UI responsive). Do not wrap I/O-bound work in Task.Run.
ValueTask (Reduced Allocations)
ValueTask<T> avoids heap allocation when the result is often available synchronously:
ValueTask<T> is ideal for methods that complete synchronously most of the time (cache hits, buffered reads) but occasionally need async I/O.
Returning a Task from Another Method
Pass through another method's task without adding overhead:
When you do not need to process the result, return the task directly. Adding unnecessary async/await creates a state machine wrapper with no benefit.
Task.FromException and Task.FromCanceled
For returning failed or canceled tasks synchronously:
Comparison Table
| Approach | When to Use | Allocations |
async/await | Method contains await calls | State machine + Task |
Task.CompletedTask | Synchronous, void return | None (cached) |
Task.FromResult<T> | Synchronous, returns a value | Minimal (some cached) |
Task.Run | CPU-bound work to offload | Thread pool + Task |
ValueTask<T> | Often sync, sometimes async | None when sync |
| Pass-through return | Delegating to another async method | None added |
Common Pitfalls
- Using
asyncwithoutawait: Anasyncmethod withoutawaitruns synchronously but still generates a state machine. UseTask.CompletedTaskorTask.FromResultinstead to avoid the overhead and suppress compiler warning CS1998. - Wrapping I/O in
Task.Run:Task.Runoffloads work to a thread pool thread, which is wasteful for I/O-bound operations that are already async. Useawaiton the native async method (e.g.,HttpClient.GetAsync) instead. - Not awaiting ValueTask correctly:
ValueTask<T>must not be awaited more than once or stored and awaited later. If you need to await aValueTaskmultiple times, convert it toTaskwith.AsTask()first. - Forgetting exception handling in pass-through: When returning a task directly (no
await), exceptions thrown before the return are not wrapped in the task — they propagate synchronously. Validation errors should useTask.FromExceptionor wrap inasync/await. - Using
Task.Resultor.Wait()on async methods: Blocking on async code with.Resultor.Wait()can deadlock in UI or ASP.NET contexts. Alwaysawaitinstead of blocking.
Summary
- Use
async/awaitwhen the method contains actual asynchronous operations - Use
Task.CompletedTaskfor synchronous void methods that returnTask - Use
Task.FromResult<T>for synchronous methods that returnTask<T> - Use
ValueTask<T>when the result is often available synchronously (cache hits) - Return another method's task directly when you do not need to process the result
- Avoid
Task.Runfor I/O-bound work — it wastes a thread pool thread
Related reading
- Different scenarios on distributed processing
- Differentiation between Synchronous Domain Events, Asynchronous Domain Events and Integration Events
- Discuss the main issues governing concurrency control in a large distributed database environment
- dispatch_after - GCD in Swift?
- Direct casting vs 'as' operator?
- Disable default global using in C 10
- dispatch_async from Grand Central Dispatch and stdasync from C11
- Dispatcher.CurrentDispatcher vs. Application.Current.Dispatcher

OOD Fundamentals
Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.
View the courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.