C#
Task\``<TResult>`\`
System.Object
type casting
asynchronous programming

Casting TResult in TaskTResult to System.Object

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In C#, Task<TResult> is not covariant — you cannot directly assign a Task<string> to a Task<object> even though string inherits from object. This is because Task<T> is a class (not an interface), and C# only supports covariance on generic interfaces and delegates. To convert Task<TResult> to Task<object>, you must unwrap the task, cast the result, and rewrap it. The common approach is using async/await with a helper method or ContinueWith.

The Problem

csharp
1Task<string> stringTask = Task.FromResult("hello");
2
3// COMPILE ERROR — Task<string> is not assignable to Task<object>
4Task<object> objectTask = stringTask;
5// Error CS0029: Cannot implicitly convert type 'Task<string>' to 'Task<object>'
6
7// Explicit cast also fails
8Task<object> objectTask = (Task<object>)stringTask;
9// Error CS0030: Cannot convert type 'Task<string>' to 'Task<object>'

This fails because Task<T> is a concrete class. Covariance (out T) only works on interfaces like IEnumerable<out T>.

csharp
1// Generic helper to convert Task<T> to Task<object>
2static async Task<object> AsObjectTask<T>(Task<T> task)
3{
4    return await task;  // TResult is implicitly cast to object
5}
6
7// Usage
8Task<string> stringTask = GetStringAsync();
9Task<object> objectTask = AsObjectTask(stringTask);
10
11object result = await objectTask;  // "hello" as object

Solution 2: ContinueWith

csharp
1Task<string> stringTask = Task.FromResult("hello");
2
3// ContinueWith transforms the result
4Task<object> objectTask = stringTask.ContinueWith(t => (object)t.Result);
5
6object result = await objectTask;  // "hello"

ContinueWith creates a new task that runs when the original completes, casting the result.

Solution 3: Extension Method

csharp
1public static class TaskExtensions
2{
3    public static async Task<object> AsObjectTask<T>(this Task<T> task)
4    {
5        return await task;
6    }
7
8    public static async Task<TTarget> Cast<TSource, TTarget>(
9        this Task<TSource> task) where TSource : TTarget
10    {
11        return await task;
12    }
13}
14
15// Usage
16Task<string> stringTask = GetStringAsync();
17Task<object> objectTask = stringTask.AsObjectTask();
18
19// Or with type constraint
20Task<object> objectTask2 = stringTask.Cast<string, object>();

Solution 4: Non-Generic Task as Common Type

If you need to store tasks of different types together, use the non-generic Task base class.

csharp
1// Task<T> inherits from Task (non-generic)
2Task<string> stringTask = GetStringAsync();
3Task<int> intTask = GetIntAsync();
4
5// Both can be stored as Task
6List<Task> tasks = new List<Task> { stringTask, intTask };
7await Task.WhenAll(tasks);
8
9// Retrieve results by casting back
10string s = ((Task<string>)tasks[0]).Result;
11int i = ((Task<int>)tasks[1]).Result;

Practical Use Case: Heterogeneous Task Collection

csharp
1public class TaskRunner
2{
3    private readonly List<Func<Task<object>>> _taskFactories = new();
4
5    public void Register<T>(Func<Task<T>> factory)
6    {
7        _taskFactories.Add(async () => await factory());
8    }
9
10    public async Task<List<object>> RunAll()
11    {
12        var tasks = _taskFactories.Select(f => f()).ToList();
13        var results = await Task.WhenAll(tasks);
14        return results.ToList();
15    }
16}
17
18// Usage
19var runner = new TaskRunner();
20runner.Register(() => GetUserAsync());     // Task<User>
21runner.Register(() => GetOrdersAsync());   // Task<List<Order>>
22runner.Register(() => GetConfigAsync());   // Task<Config>
23
24List<object> results = await runner.RunAll();

Why Task<T> Is Not Covariant

csharp
1// IEnumerable<T> IS covariant (declared as IEnumerable<out T>)
2IEnumerable<string> strings = new List<string>();
3IEnumerable<object> objects = strings;  // Works!
4
5// Task<T> is a CLASS — classes cannot declare covariance
6// Only interfaces and delegates support out/in variance in C#
7
8// If Task<T> were covariant, this would be unsafe:
9// Task<object> could be awaited and then incorrectly cast
10// The type system cannot guarantee safety at compile time

Common Pitfalls

  • Assuming Task<Derived> is assignable to Task<Base>: C# generics on classes are invariant. Task<string> cannot be assigned to Task<object> even though string is an object. Always use an explicit conversion helper.
  • Using .Result inside the cast: ContinueWith(t => (object)t.Result) blocks if the original task has not completed. In UI or ASP.NET contexts, this can deadlock. Prefer async/await based conversion to avoid blocking.
  • Forgetting to propagate exceptions: A simple ContinueWith(t => (object)t.Result) does not handle faulted tasks gracefully — accessing .Result on a faulted task throws AggregateException. The async/await helper naturally propagates the original exception.
  • Creating unnecessary wrapper tasks: Each conversion creates a new Task<object> wrapper. In hot paths or tight loops, this adds allocation overhead. If performance is critical, consider redesigning the API to avoid the conversion.
  • Using non-generic Task and losing type safety: Storing everything as Task and casting back with ((Task<string>)task).Result is fragile — a wrong cast throws InvalidCastException at runtime. Prefer strongly-typed collections or discriminated unions where possible.

Summary

  • Task<TResult> is not covariant — Task<string> cannot be assigned to Task<object>
  • Use an async helper method: async Task<object> AsObjectTask<T>(Task<T> task) => await task;
  • Use ContinueWith(t => (object)t.Result) as an alternative (but handle exceptions carefully)
  • Create an extension method for reusable task casting across your codebase
  • Prefer async/await conversion over ContinueWith to properly propagate exceptions

Course illustration
Course illustration

All Rights Reserved.