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
This fails because Task<T> is a concrete class. Covariance (out T) only works on interfaces like IEnumerable<out T>.
Solution 1: Async Helper Method (Recommended)
Solution 2: ContinueWith
ContinueWith creates a new task that runs when the original completes, casting the result.
Solution 3: Extension Method
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.
Practical Use Case: Heterogeneous Task Collection
Why Task<T> Is Not Covariant
Common Pitfalls
- Assuming
Task<Derived>is assignable toTask<Base>: C# generics on classes are invariant.Task<string>cannot be assigned toTask<object>even thoughstringis anobject. Always use an explicit conversion helper. - Using
.Resultinside the cast:ContinueWith(t => (object)t.Result)blocks if the original task has not completed. In UI or ASP.NET contexts, this can deadlock. Preferasync/awaitbased conversion to avoid blocking. - Forgetting to propagate exceptions: A simple
ContinueWith(t => (object)t.Result)does not handle faulted tasks gracefully — accessing.Resulton a faulted task throwsAggregateException. Theasync/awaithelper 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
Taskand losing type safety: Storing everything asTaskand casting back with((Task<string>)task).Resultis fragile — a wrong cast throwsInvalidCastExceptionat runtime. Prefer strongly-typed collections or discriminated unions where possible.
Summary
Task<TResult>is not covariant —Task<string>cannot be assigned toTask<object>- Use an
asynchelper 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/awaitconversion overContinueWithto properly propagate exceptions

