Given three tasks - FeedCat(), SellHouse() and BuyCar(), there are two interesting cases: either they all complete synchronously (for some reason, perhaps caching or an error), or they don't.
Let's say we have, from the question:
Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();
    // what here?
}
Now, a simple approach would be:
Task.WhenAll(x, y, z);
but ... that isn't convenient for processing the results; we'd typically want to await that:
async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();
    await Task.WhenAll(x, y, z);
    // presumably we want to do something with the results...
    return DoWhatever(x.Result, y.Result, z.Result);
}
but this does lots of overhead and allocates various arrays (including the params Task[] array) and lists (internally). It works, but it isn't great IMO. In many ways it is simpler to use an async operation and just await each in turn:
async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();
    // do something with the results...
    return DoWhatever(await x, await y, await z);
}
Contrary to some of the comments above, using await instead of Task.WhenAll makes no difference to how the tasks run (concurrently, sequentially, etc). At the highest level, Task.WhenAll predates good compiler support for async/await, and was useful when those things didn't exist. It is also useful when you have an arbitrary array of tasks, rather than 3 discreet tasks.
But: we still have the problem that async/await generates a lot of compiler noise for the continuation. If it is likely that the tasks might actually complete synchronously, then we can optimize this by building in a synchronous path with an asynchronous fallback:
Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();
    if(x.Status == TaskStatus.RanToCompletion &&
       y.Status == TaskStatus.RanToCompletion &&
       z.Status == TaskStatus.RanToCompletion)
        return Task.FromResult(
          DoWhatever(a.Result, b.Result, c.Result));
       // we can safely access .Result, as they are known
       // to be ran-to-completion
    return Awaited(x, y, z);
}
async Task Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
    return DoWhatever(await x, await y, await z);
}
This "sync path with async fallback" approach is increasingly common especially in high performance code where synchronous completions are relatively frequent. Note it won't help at all if the completion is always genuinely asynchronous.
Additional things that apply here:
- with recent C#, a common pattern is for the - asyncfallback method is commonly implemented as a local function:
 - Task<string> DoTheThings() {
    async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
        return DoWhatever(await a, await b, await c);
    }
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();
    if(x.Status == TaskStatus.RanToCompletion &&
       y.Status == TaskStatus.RanToCompletion &&
       z.Status == TaskStatus.RanToCompletion)
        return Task.FromResult(
          DoWhatever(a.Result, b.Result, c.Result));
       // we can safely access .Result, as they are known
       // to be ran-to-completion
    return Awaited(x, y, z);
}
 
- prefer - ValueTask<T>to- Task<T>if there is a good chance of things ever completely synchronously with many different return values:
 - ValueTask<string> DoTheThings() {
    async ValueTask<string> Awaited(ValueTask<Cat> a, Task<House> b, Task<Tesla> c) {
        return DoWhatever(await a, await b, await c);
    }
    ValueTask<Cat> x = FeedCat();
    ValueTask<House> y = SellHouse();
    ValueTask<Tesla> z = BuyCar();
    if(x.IsCompletedSuccessfully &&
       y.IsCompletedSuccessfully &&
       z.IsCompletedSuccessfully)
        return new ValueTask<string>(
          DoWhatever(a.Result, b.Result, c.Result));
       // we can safely access .Result, as they are known
       // to be ran-to-completion
    return Awaited(x, y, z);
}
 
- if possible, prefer - IsCompletedSuccessfullyto- Status == TaskStatus.RanToCompletion; this now exists in .NET Core for- Task, and everywhere for- ValueTask<T>