async programming
C#
Task method
asynchronous method
Task implementation

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.

Browse interview questions

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:

csharp
1public async Task<string> FetchDataAsync(string url)
2{
3    using var client = new HttpClient();
4    string result = await client.GetStringAsync(url);
5    return result;
6}
7
8public async Task SaveDataAsync(string data)
9{
10    await File.WriteAllTextAsync("output.txt", data);
11}

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:

csharp
1public interface IHandler
2{
3    Task HandleAsync(string message);
4}
5
6// Synchronous implementation
7public class LogHandler : IHandler
8{
9    public Task HandleAsync(string message)
10    {
11        Console.WriteLine(message); // Synchronous work
12        return Task.CompletedTask;  // Returns an already-completed task
13    }
14}

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:

csharp
1public interface ICache
2{
3    Task<string> GetAsync(string key);
4}
5
6// In-memory cache — no async work
7public class MemoryCache : ICache
8{
9    private readonly Dictionary<string, string> _store = new();
10
11    public Task<string> GetAsync(string key)
12    {
13        _store.TryGetValue(key, out var value);
14        return Task.FromResult(value);  // Wraps the value in a completed Task
15    }
16}

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:

csharp
1public Task<int> ComputeAsync(int[] data)
2{
3    return Task.Run(() =>
4    {
5        // CPU-intensive work on a thread pool thread
6        return data.Sum();
7    });
8}

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:

csharp
1public ValueTask<int> GetCountAsync()
2{
3    if (_cache.TryGetValue("count", out int count))
4    {
5        return new ValueTask<int>(count); // No allocation — returns a struct
6    }
7
8    return new ValueTask<int>(FetchCountFromDbAsync()); // Falls back to async
9}
10
11private async Task<int> FetchCountFromDbAsync()
12{
13    // Actual async database call
14    await Task.Delay(100); // Simulating DB
15    return 42;
16}

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:

csharp
1public class UserService
2{
3    private readonly IUserRepository _repo;
4
5    // Pass through — no async/await needed
6    public Task<User> GetUserAsync(int id)
7    {
8        return _repo.FindByIdAsync(id);
9    }
10
11    // Only use async/await if you need to do something with the result
12    public async Task<UserDto> GetUserDtoAsync(int id)
13    {
14        var user = await _repo.FindByIdAsync(id);
15        return new UserDto(user.Name, user.Email);
16    }
17}

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:

csharp
1public Task<string> ValidateAsync(string input)
2{
3    if (string.IsNullOrEmpty(input))
4    {
5        return Task.FromException<string>(new ArgumentException("Input required"));
6    }
7
8    return Task.FromResult(input.Trim());
9}
10
11public Task ProcessAsync(CancellationToken token)
12{
13    if (token.IsCancellationRequested)
14    {
15        return Task.FromCanceled(token);
16    }
17
18    return DoActualWorkAsync(token);
19}

Comparison Table

ApproachWhen to UseAllocations
async/awaitMethod contains await callsState machine + Task
Task.CompletedTaskSynchronous, void returnNone (cached)
Task.FromResult<T>Synchronous, returns a valueMinimal (some cached)
Task.RunCPU-bound work to offloadThread pool + Task
ValueTask<T>Often sync, sometimes asyncNone when sync
Pass-through returnDelegating to another async methodNone added

Common Pitfalls

  • Using async without await: An async method without await runs synchronously but still generates a state machine. Use Task.CompletedTask or Task.FromResult instead to avoid the overhead and suppress compiler warning CS1998.
  • Wrapping I/O in Task.Run: Task.Run offloads work to a thread pool thread, which is wasteful for I/O-bound operations that are already async. Use await on 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 a ValueTask multiple times, convert it to Task with .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 use Task.FromException or wrap in async/await.
  • Using Task.Result or .Wait() on async methods: Blocking on async code with .Result or .Wait() can deadlock in UI or ASP.NET contexts. Always await instead of blocking.

Summary

  • Use async/await when the method contains actual asynchronous operations
  • Use Task.CompletedTask for synchronous void methods that return Task
  • Use Task.FromResult<T> for synchronous methods that return Task<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.Run for I/O-bound work — it wastes a thread pool thread

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track 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.

Browse interview questions