TaskFactory.CompleteWhenBoth
You don't need to write this code yourself, there is already a method that does something very similar: Task.Factory.ContinueWhenAll().

ContinueWhenAll() doesn't directly support working with Tasks that return different types, but you can create a helper method that does:

public static Task&lt;R&gt; ContinueWhenBoth&lt;T1, T2, R&gt;(
    this TaskFactory&lt;R&gt; factory, Task&lt;T1&gt; t1, Task&lt;T2&gt; t2, Func&lt;T1, T2, R&gt; f)
{
    return factory.ContinueWhenAll(new Task[] { t1, t2 }, _ =&gt; f(t1.Result, t2.Result));
}


If you didn't want to use closure for performance reasons, you could use the array of Tasks and casting:

return factory.ContinueWhenAll(
    new Task[] { t1, t2 }, tasks =&gt; f(((Task&lt;T1&gt;)tasks[0]).Result, ((Task&lt;T2&gt;)t2).Result));




t1.ContinueWith(t =&gt; { if(t2.IsCompleted) result.TrySetResult(f(t1.Result, t2.Result)); });
t2.ContinueWith(t =&gt; { if(t1.IsCompleted) result.TrySetResult(f(t1.Result, t2.Result)); });


This code has a race condition: if t1 and t2 complete at the same time, then f might be executed twice. I don't think that's a good idea, since f might take a long time, or, worse, it might have side-effects.

if(Interlocked.Decrement(ref tasksLeft) == 0 &amp;&amp; t2.IsCompleted) 


The Decrement() here avoids the race condition from the previous code. But it also means that the check of t2.IsCompleted will always succeed, so you should remove it.



But your code also has a significant issue: if t1 or t2 fail with an exception, your resulting Task will never complete. (And on .Net 4.0, it will also crash the process some time in the future, because there is an uncaught exception in a Task.)