Start Debugging

Fix: CS4014 "Because this call is not awaited, execution of the current method continues" in C#

CS4014 means you called a Task-returning method without awaiting it. Add await, or discard with _ = if fire-and-forget is truly intended, and handle exceptions.

CS4014 fires when you call a method that returns a Task or Task<T> from inside an async method but do not await it. The compiler warns that the current method keeps running before the call finishes. Fix it by adding await to the call, which is what you want the vast majority of the time. If the fire-and-forget behavior is genuinely intended, make that explicit by assigning the result to a discard (_ = SomeAsyncCall();), and make sure something handles the exceptions the task might throw. This is verified against C# 14 on .NET 11; the diagnostic has behaved this way since async/await shipped in C# 5, so the guidance applies to every modern .NET version.

The error in context

The compiler emits this as a warning, not an error:

warning CS4014: Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

Note the word warning. CS4014 does not stop the build by default, which is exactly why it is dangerous: it is easy to ignore, and the bug it points at (a task running unobserved, its exceptions silently swallowed) does not show up until production. Many teams promote it to an error with <TreatWarningsAsErrors>true</TreatWarningsAsErrors> or the narrower <WarningsAsErrors>CS4014</WarningsAsErrors> in the .csproj precisely so that an accidentally-dropped await cannot slip through code review.

The warning only appears inside an async method. The compiler reasons that if you bothered to mark the enclosing method async, an unawaited task call is almost certainly a mistake. Call the same method from a non-async method and you get no CS4014 at all, which is a related trap covered below.

Why this happens

An async method that returns Task starts running synchronously and returns a task object the moment it hits its first incomplete await. The task represents the still-running operation. When you write DoWorkAsync(); as a bare statement, you throw that task object away. Two things follow, and both are bad.

First, execution does not wait. The line after your call runs immediately, before DoWorkAsync has finished. Any code that depends on the operation completing, a database write, a file flush, a cache update, now races against it. This is the “execution of the current method continues” half of the message.

Second, and worse, exceptions vanish. When you await a task, any exception it captured is re-thrown into your method so your try/catch can see it. Drop the task and there is nothing to re-throw into. The exception sits on the discarded task object, unobserved, until the garbage collector eventually finalizes it. In .NET Framework 4.0 that would crash the process; since 4.5 and on all of modern .NET the default is to swallow it entirely. So an unawaited task that fails looks exactly like success from the caller’s point of view. That silent failure is the real reason CS4014 exists, and why “just suppress the warning” is almost never the right move.

The one case the compiler cannot help with: async void. If DoWorkAsync returns void instead of Task, there is no task to await and no CS4014, but all the same problems apply plus one more, an exception from an async void method is raised on the synchronization context and typically tears down the process. That is a separate diagnosis, covered in async void vs async Task in C#.

Minimal repro

The smallest code that triggers CS4014:

// .NET 11, C# 14
public class OrderService
{
    public async Task PlaceOrderAsync(Order order)
    {
        SaveAsync(order);          // CS4014: not awaited
        Console.WriteLine("Order placed");   // runs before SaveAsync finishes
    }

    private async Task SaveAsync(Order order)
    {
        await Task.Delay(100);     // stand-in for a real DB write
        throw new InvalidOperationException("DB down");
    }
}

Two bugs in four lines. "Order placed" prints before the save has run, and the InvalidOperationException is never seen by anyone: PlaceOrderAsync completes successfully as far as its caller can tell. The warning is the only signal you get at compile time that the order was never actually saved.

A common variant hides the call inside a Task.Run or an event handler, where it is easier to miss:

// .NET 11, C# 14
button.Clicked += async (s, e) =>
{
    RefreshAsync();   // CS4014: fire-and-forget by accident
};

Fix, in detail

Work through these in order. The first fix is correct for almost every real occurrence; the rest are for the genuine exceptions.

1. Add await (the fix you want 95% of the time)

If you are inside an async method, the intent is nearly always to wait for the call. Add await:

// .NET 11, C# 14
public async Task PlaceOrderAsync(Order order)
{
    await SaveAsync(order);        // waits, and re-throws any exception
    Console.WriteLine("Order placed");
}

Now "Order placed" prints only after the save completes, and if SaveAsync throws, the exception propagates out of PlaceOrderAsync so a caller’s try/catch (or the ASP.NET Core pipeline) can handle it. This single change fixes both the ordering bug and the swallowed-exception bug at once. Reach for the other options only when you can articulate why waiting is wrong.

2. Await multiple calls together with Task.WhenAll

If the reason you did not await was that you wanted several operations to run concurrently, do not drop the tasks, collect them and await them together:

// .NET 11, C# 14
public async Task NotifyAllAsync(IEnumerable<User> users)
{
    var tasks = users.Select(u => SendEmailAsync(u));
    await Task.WhenAll(tasks);     // all run concurrently, all awaited
}

Task.WhenAll gives you the concurrency without giving up observation: it starts every task, then completes when the last one does, and re-throws if any of them failed. This is the correct pattern for fan-out work and it clears CS4014 because the tasks are awaited. For the trade-offs between this and other parallel approaches, see Parallel.ForEach vs Parallel.ForEachAsync vs Task.WhenAll.

3. Return the task instead of awaiting it

If your method is a thin pass-through that does nothing after the call, you often do not need async/await at all. Drop both and return the task:

// .NET 11, C# 14
public Task PlaceOrderAsync(Order order)
{
    return SaveAsync(order);       // caller awaits; no state machine here
}

This removes the async modifier, so CS4014 no longer applies (the warning is only raised inside async methods), and it skips the overhead of generating a state machine for a method that does not need one. The caller still gets a task to await. The one caveat: without await, exceptions surface when the caller awaits the returned task rather than at the point of the call, and a using block would dispose its resource before the returned task completes. Use this only for genuine pass-throughs.

4. Explicitly discard, only when fire-and-forget is truly intended

Sometimes you really do want to start work and not wait, logging a metric, warming a cache, kicking off a best-effort notification. In that case make the intent unmistakable with a discard, and handle the exceptions yourself so they are not lost:

// .NET 11, C# 14
public void OnUserLoggedIn(User user)
{
    _ = LogAnalyticsAsync(user);   // intentional fire-and-forget, warning cleared
}

private async Task LogAnalyticsAsync(User user)
{
    try
    {
        await _analytics.RecordAsync(user.Id);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Analytics failed for {UserId}", user.Id);
    }
}

The _ = discard tells both the compiler and the next reader “yes, I meant to not await this.” Critically, the discard clears the warning but does not fix the swallowed-exception problem, so the try/catch inside LogAnalyticsAsync is doing the real work. A fire-and-forget task with no internal exception handling is a crash or a silent data-loss bug waiting to happen.

Even with a discard, raw fire-and-forget in a web app is fragile: the request can complete and the host can start shutting down while your task is mid-flight, cancelling or killing it. For anything that must actually finish, do not fire-and-forget from a request at all; hand the work to a background queue. That pattern is covered in how to run fire-and-forget work safely in ASP.NET Core with BackgroundService.

Gotchas and variants

A few situations produce CS4014, or hide it, for reasons the message does not spell out:

The mental model to keep: CS4014 is the compiler telling you a task is about to run unobserved. Decide which is actually true, then act on it. You meant to wait (add await), you meant to run several things concurrently (Task.WhenAll), the method is a pass-through (return the task), or you genuinely want fire-and-forget (discard with _ = and handle exceptions inside). Suppressing the warning with a discard while leaving the exceptions unhandled just converts a compile-time nudge into a silent runtime failure, which is the exact bug the warning exists to prevent.

Sources

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Back