Asynchronous programming
Task.Run
C#
Task return value
multithreading

Getting return value from Task.Run

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Task.Run returns a Task when the work produces no value and a Task<T> when the work returns a result. That means the normal way to get a return value is to await the Task<T>. The language already knows how to unwrap the final result for you, so most of the complexity in this topic is really about choosing between await, .Result, and synchronous blocking.

Return a Value From the Delegate

If the lambda returns something, Task.Run gives you Task<T>.

csharp
1using System;
2using System.Threading.Tasks;
3
4class Program
5{
6    static async Task Main()
7    {
8        Task<int> task = Task.Run(() =>
9        {
10            int a = 20;
11            int b = 22;
12            return a + b;
13        });
14
15        int result = await task;
16        Console.WriteLine(result);  // 42
17    }
18}

This is the standard pattern. The task represents work in progress, and await gives you the finished value once the work completes.

await Is the Preferred Retrieval Method

In modern C#, await is almost always the best way to read the result.

csharp
int result = await Task.Run(() => ExpensiveCalculation());

Why this is preferred:

  • it does not block the calling thread while waiting
  • exceptions are surfaced naturally
  • the code stays readable

If the surrounding method can be async, there is usually no reason to avoid await.

Understand .Result and .Wait()

You can also retrieve the value synchronously:

csharp
Task<int> task = Task.Run(() => 42);
int result = task.Result;

This works, but it blocks the current thread until the task completes. In console apps, that may be acceptable. In UI apps and older ASP.NET synchronization contexts, blocking can create responsiveness problems or deadlock patterns.

.Wait() behaves similarly for tasks without results:

csharp
Task task = Task.Run(() => Console.WriteLine("Working"));
task.Wait();

Use blocking only when you truly need synchronous behavior and understand the cost.

Exceptions and Return Values

One reason await feels cleaner is exception handling. With await, ordinary try and catch code works naturally.

csharp
1try
2{
3    int result = await Task.Run(() =>
4    {
5        throw new InvalidOperationException("Calculation failed");
6    });
7}
8catch (InvalidOperationException ex)
9{
10    Console.WriteLine(ex.Message);
11}

If you use .Result or .Wait(), exceptions can arrive wrapped in AggregateException, which is less pleasant to work with.

Do Not Use Task.Run for Everything

A common misunderstanding is that Task.Run is the way to make any method asynchronous. It is not. Task.Run is mainly useful for offloading CPU-bound work to the thread pool.

For true asynchronous I/O, prefer an API that is already async:

csharp
string text = await File.ReadAllTextAsync("data.txt");

That is usually better than wrapping a synchronous file read in Task.Run.

Returning Complex Types

The result does not have to be a number. Any type works.

csharp
1record User(string Name, int Age);
2
3User user = await Task.Run(() => new User("Ana", 30));
4Console.WriteLine(user.Name);

The pattern stays the same because Task<T> is generic.

Keep the Calling Code Consistent

A good rule is simple:

  • if you start with async code, stay async and await the result
  • avoid mixing await with synchronous .Result unless you have a clear reason
  • use Task.Run mainly for CPU-bound work, not as a generic “make it async” wrapper

That keeps code easier to reason about and avoids thread-blocking surprises.

Common Pitfalls

A common mistake is forgetting that a value-returning Task.Run produces Task<T>, not T. You only get the final value after awaiting or synchronously blocking on the task.

Another issue is using .Result inside UI or request-handling code where blocking the thread is harmful. If the method can be async, prefer await.

Developers also sometimes wrap naturally asynchronous APIs inside Task.Run, which adds thread-pool work without real benefit.

Finally, do not ignore exceptions. A faulted task still has a “result type,” but trying to consume it without proper handling will fail at runtime.

Summary

  • 'Task.Run returns Task<T> when the delegate returns a value.'
  • The normal way to get that value is await.
  • '.Result works but blocks the current thread and is often the worse choice.'
  • Use Task.Run mainly for CPU-bound work, not as a blanket async strategy.
  • Treat exceptions and synchronous blocking carefully when consuming task results.

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

All Rights Reserved.