Unnecessary async/await when await is last?

You're close. It means that you can write it like this:

public Task ProcessAsync()
{
    // some sync code
    return SimulateWork();
}

That way you don't "pay" for the overhead of marking the method as async but you still keep the ability to await that whole operation.


So, you think the await below is redundant, as the question's title implies:

public async Task ProcessAsync()
{
    Task<string> workTask = SimulateWork();
    await workTask; //i don't care about the result , and I don't have any further 
}

First of all, I assume that under "when await is last" you mean "when the await is the only await". It's got to be that, because otherwise the following simply would not compile:

public async Task ProcessAsync()
{
    await Task.Delay(1000);
    Task<string> workTask = SimulateWork();
    return workTask; 
}

Now, if it's the only await, you can indeed optimize it like this:

public Task ProcessAsync()
{
    Task<string> workTask = SimulateWork();
    return workTask; 
}

However, it would give you completely different exception propagation behavior, which may have some unexpected side effects. The thing is, now exceptions may be thrown on the caller's stack, depending on how SimulateWork is internally implemented. I posted a detailed explanation of this behavior. This normally never happens with async Task/Task<> methods, where exception is stored inside the returned Task object. It still may happen for an async void method, but that's a different story.

So, if your caller code is ready for such differences in exception propagation, it may be a good idea to skip async/await wherever you can and simply return a Task instead.

Another matter is if you want to issue a fire-and-forget call. Usually, you still want to track the status of the fired task somehow, at least for the reason of handling task exceptions. I could not imagine a case where I would really not care if the task never completes, even if all it does is logging.

So, for fire-and-forget I usually use a helper async void method which stores the pending task somewhere for later observation, e.g.:

readonly object _syncLock = new Object();
readonly HashSet<Task> _pendingTasks = new HashSet<Task>();

async void QueueTaskAsync(Task task)
{
    // keep failed/cancelled tasks in the list
    // they will be observed outside
    lock (_syncLock)
        _pendingTasks.Add(task);

    try
    {
        await task;
    }
    catch
    {
        // is it not task's exception?
        if (!task.IsCanceled && !task.IsFaulted)
            throw; // re-throw

        // swallow, but do not remove the faulted/cancelled task from _pendingTasks 
        // the error will be observed later, when we process _pendingTasks,
        // e.g.: await Task.WhenAll(_pendingTasks.ToArray())
        return;
    }

    // remove the successfully completed task from the list
    lock (_syncLock)
        _pendingTasks.Remove(task);
}

You'd call it like this:

public Task ProcessAsync()
{
    QueueTaskAsync(SimulateWork());
}

The goal is to throw fatal exceptions (e.g., out-of-memory) immediately on the current thread's synchronization context, while task result/error processing is deferred until appropriate.

There's been an interesting discussion of using tasks with fire-and-forget here.