Исправление: CS4014 "Because this call is not awaited, execution of the current method continues" в C#
CS4014 означает, что вы вызвали метод, возвращающий Task, не ожидая его. Добавьте await или отбросьте через _ = при осознанном fire-and-forget и обработайте исключения.
CS4014 возникает, когда вы вызываете метод, возвращающий Task или Task<T>, изнутри async-метода, но не ожидаете его через await. Компилятор предупреждает, что текущий метод продолжает выполняться до завершения вызова. Исправьте это, добавив await к вызову, что и требуется в подавляющем большинстве случаев. Если поведение fire-and-forget действительно задумано, сделайте это явным, присвоив результат отбрасыванию (_ = SomeAsyncCall();), и убедитесь, что что-то обрабатывает исключения, которые задача может выбросить. Это проверено на C# 14 в .NET 11; диагностика ведёт себя так с тех пор, как async/await появились в C# 5, поэтому руководство применимо к любой современной версии .NET.
Ошибка в контексте
Компилятор выдаёт это как предупреждение, а не как ошибку:
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.
Обратите внимание на слово warning. CS4014 по умолчанию не останавливает сборку, и именно поэтому оно опасно: его легко проигнорировать, а ошибка, на которую оно указывает (задача выполняется без наблюдения, её исключения молча проглатываются), проявляется только в продакшене. Многие команды повышают его до ошибки через <TreatWarningsAsErrors>true</TreatWarningsAsErrors> или более узкое <WarningsAsErrors>CS4014</WarningsAsErrors> в .csproj именно для того, чтобы случайно опущенный await не прошёл через код-ревью.
Предупреждение появляется только внутри async-метода. Компилятор рассуждает так: если вы уже потрудились пометить охватывающий метод как async, то неожидаемый вызов задачи почти наверняка является недосмотром. Вызовите тот же метод из не-async-метода, и вы не получите CS4014 вовсе, что является связанной ловушкой, рассматриваемой ниже.
Почему это происходит
async-метод, возвращающий Task, начинает выполняться синхронно и возвращает объект задачи в тот момент, когда доходит до своего первого незавершённого await. Задача представляет всё ещё выполняющуюся операцию. Когда вы пишете DoWorkAsync(); как отдельный оператор, вы выбрасываете этот объект задачи. Из этого следуют две вещи, и обе плохи.
Во-первых, выполнение не ждёт. Строка после вашего вызова выполняется немедленно, до того как DoWorkAsync завершится. Любой код, зависящий от завершения операции, запись в базу данных, сброс файла, обновление кеша, теперь конкурирует с ней. Это половина сообщения “execution of the current method continues”.
Во-вторых, и хуже, исключения исчезают. Когда вы ожидаете задачу через await, любое перехваченное ею исключение повторно выбрасывается в ваш метод, чтобы ваш try/catch мог его увидеть. Отбросьте задачу, и повторно выбрасывать будет некуда. Исключение остаётся на отброшенном объекте задачи, без наблюдения, пока сборщик мусора в конце концов не финализирует его. В .NET Framework 4.0 это приводило к падению процесса; начиная с 4.5 и во всех современных версиях .NET по умолчанию оно проглатывается целиком. Так что неожидаемая задача, которая завершилась неудачей, выглядит точно как успех с точки зрения вызывающего кода. Этот молчаливый сбой и есть настоящая причина существования CS4014, и почему “просто подавить предупреждение” почти никогда не является правильным ходом.
Единственный случай, где компилятор не может помочь: async void. Если DoWorkAsync возвращает void вместо Task, ожидать нечего и CS4014 нет, но все те же проблемы применимы плюс ещё одна: исключение из async void-метода выбрасывается в контексте синхронизации и обычно обрушивает процесс. Это отдельная диагностика, рассмотренная в async void vs async Task в C#.
Минимальное воспроизведение
Наименьший код, вызывающий 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");
}
}
Две ошибки в четырёх строках. "Order placed" печатается до того, как запись выполнилась, и InvalidOperationException никто не видит: PlaceOrderAsync завершается успешно, насколько может судить вызывающий код. Предупреждение остаётся единственным сигналом во время компиляции о том, что заказ так и не был сохранён.
Распространённый вариант прячет вызов внутри Task.Run или обработчика события, где его легче пропустить:
// .NET 11, C# 14
button.Clicked += async (s, e) =>
{
RefreshAsync(); // CS4014: fire-and-forget by accident
};
Исправление в деталях
Пройдите по ним по порядку. Первое подходит почти для каждого реального случая; остальные предназначены для настоящих исключений.
1. Добавить await (исправление, которое нужно в 95% случаев)
Если вы находитесь внутри async-метода, намерение почти всегда состоит в том, чтобы дождаться вызова. Добавьте await:
// .NET 11, C# 14
public async Task PlaceOrderAsync(Order order)
{
await SaveAsync(order); // waits, and re-throws any exception
Console.WriteLine("Order placed");
}
Теперь "Order placed" печатается только после завершения записи, а если SaveAsync выбросит исключение, оно распространится из PlaceOrderAsync, чтобы try/catch вызывающего кода (или конвейер ASP.NET Core) мог его обработать. Это единственное изменение исправляет сразу и ошибку порядка, и ошибку проглоченного исключения. Прибегайте к другим вариантам только тогда, когда можете сформулировать, почему ожидание неверно.
2. Ожидать несколько вызовов вместе через Task.WhenAll
Если причина, по которой вы не ожидали через await, была в том, что вы хотели, чтобы несколько операций выполнялись параллельно, не отбрасывайте задачи, а соберите их и ожидайте вместе:
// .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 даёт вам параллелизм, не отказываясь от наблюдения: он запускает каждую задачу, затем завершается, когда завершается последняя, и повторно выбрасывает, если любая из них завершилась неудачей. Это правильный шаблон для веерной работы, и он устраняет CS4014, потому что задачи ожидаются. О компромиссах между этим и другими параллельными подходами см. Parallel.ForEach vs Parallel.ForEachAsync vs Task.WhenAll.
3. Вернуть задачу вместо её ожидания
Если ваш метод представляет собой тонкий проброс, который ничего не делает после вызова, часто вам вовсе не нужны async/await. Уберите оба и верните задачу:
// .NET 11, C# 14
public Task PlaceOrderAsync(Order order)
{
return SaveAsync(order); // caller awaits; no state machine here
}
Это убирает модификатор async, поэтому CS4014 больше не применяется (предупреждение выдаётся только внутри async-методов), и избавляет от накладных расходов на генерацию конечного автомата для метода, которому он не нужен. Вызывающий код всё так же получает задачу для ожидания через await. Единственная оговорка: без await исключения всплывают, когда вызывающий код ожидает возвращённую задачу, а не в точке вызова, а блок using освободил бы свой ресурс до завершения возвращённой задачи. Используйте это только для настоящих пробросов.
4. Явно отбросить, только когда fire-and-forget действительно задуман
Иногда вы действительно хотите запустить работу и не ждать: записать метрику, прогреть кеш, инициировать уведомление по принципу наилучших усилий. В этом случае сделайте намерение недвусмысленным через отбрасывание и обработайте исключения сами, чтобы они не потерялись:
// .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);
}
}
Отбрасывание _ = сообщает и компилятору, и следующему читателю: “да, я намеренно не ожидаю это”. Важно: отбрасывание устраняет предупреждение, но не исправляет проблему проглоченного исключения, поэтому try/catch внутри LogAnalyticsAsync делает настоящую работу. Задача fire-and-forget без внутренней обработки исключений представляет собой падение или молчаливую потерю данных, ожидающую своего часа.
Даже с отбрасыванием чистый fire-and-forget в веб-приложении хрупок: запрос может завершиться, а хост может начать выключение, пока ваша задача ещё выполняется, отменяя или убивая её. Для всего, что действительно должно завершиться, не делайте fire-and-forget из запроса вовсе; передайте работу фоновой очереди. Этот шаблон рассмотрен в как безопасно выполнять fire-and-forget-работу в ASP.NET Core с BackgroundService.
Подводные камни и варианты
Некоторые ситуации порождают CS4014 или скрывают его по причинам, которые сообщение не разъясняет:
-
Нет предупреждения вне
async-метода. Точно такой же неожидаемый вызов в обычном (не-async) методе не порождает никакогоCS4014. Компилятор предполагает, что не-async-метод может законно запускать фоновую работу. Вот почему ошибки проникают, когда кто-то одновременно убираетawaitи охватывающий модификаторasync: предупреждение, которое поймало бы это, исчезает вместе с модификатором. Если вы полагаетесь на предупреждение как на страховочную сетку, держите<WarningsAsErrors>CS4014</WarningsAsErrors>включённым и с подозрением относитесь к любому отдельному вызову, возвращающему Task. -
Отбрасывание заглушает предупреждение, но не ошибку.
_ = DoAsync();устраняетCS4014, но еслиDoAsyncвыбросит исключение и ничто внутри не перехватит его, исключение всё равно потеряется. Отбрасывание представляет собой заявление о намерении, а не исправление для ненаблюдаемых исключений. Всегда сопровождайте fire-and-forget внутреннимtry/catch. -
Блокировка через
.Resultили.Wait()не является исправлением. Замена отсутствующегоawaitнаSaveAsync(order).Resultубирает предупреждение и блокирует до завершения задачи, но в контексте синхронизации UI или классического ASP.NET это приводит к взаимной блокировке, а везде остальном тратит поток. Если вас тянет заблокировать, потому что вы не можете сделать вызывающий кодasync, сначала прочитайте про взаимную блокировку, которую вы получаете при вызове .Result или .Wait() на async-методе. -
Task.Run(() => FooAsync())проглатывает внутреннюю задачу. Передачаasync-лямбды вTask.Run, где делегат возвращаетvoid(async void-лямбда), даёт вамTask, которая завершается, когда лямбда начинает своё первое await, а не когда внутренняя работа заканчивается. ПредпочитайтеTask.Run(FooAsync)илиTask.Run(async () => await FooAsync()), чтобы возвращённая задача отслеживала настоящую работу, и затем ожидайте эту задачу черезawait. -
CancellationToken, который вы никогда не пробрасываете. Частая причина затянувшейся fire-and-forget-задачи в том, что у метода нет способа быть отменённым, поэтому он продолжает выполняться после того, как вызывающий код пошёл дальше. Если ваш неожидаемый вызов является фоновой работой, проведите в неё токен, чтобы её можно было чисто остановить; см. как пробросить CancellationToken через async-методы. -
Пересечение анализаторов с CA2012 и VSTHRD110. Помимо
CS4014компилятора, анализаторы .NET (CA2012дляValueTask) и анализаторы потоков Visual Studio (VSTHRD110, “observe the awaitable result”) отмечают тот же класс недосмотров в большем числе мест, включая некоторые не-async-методы, гдеCS4014молчит. Если вы хотите проверку неожидаемых задач повсюду, а не только внутриasync-методов, включение этих анализаторов закрывает брешь, которую оставляет предупреждение компилятора.
Ментальная модель, которую стоит держать в голове: CS4014 представляет собой компилятор, сообщающий вам, что задача вот-вот выполнится без наблюдения. Решите, что на самом деле верно, и затем действуйте соответственно. Вы хотели дождаться (добавьте await), вы хотели запустить несколько вещей параллельно (Task.WhenAll), метод является пробросом (верните задачу) или вы действительно хотите fire-and-forget (отбросьте через _ = и обработайте исключения внутри). Подавление предупреждения отбрасыванием при оставленных без обработки исключениях лишь превращает подсказку во время компиляции в молчаливый сбой во время выполнения, что и есть та самая ошибка, ради предотвращения которой существует предупреждение.
Похожее
- async void vs async Task в C#: когда какой правильный о том, почему возвращающая
voidверсия этого вызова ещё опаснее и не порождает предупреждения. - Исправление: взаимная блокировка при вызове .Result или .Wait() на async-методе в C# о том, почему блокировка не является допустимым способом заглушить CS4014.
- Как безопасно выполнять fire-and-forget-работу в ASP.NET Core с BackgroundService о правильном способе запуска работы, которая должна пережить запрос.
- Parallel.ForEach vs Parallel.ForEachAsync vs Task.WhenAll для выбора способа параллельного выполнения множества асинхронных операций.
- Как пробросить CancellationToken через async-методы в .NET 11, чтобы сделать фоновую работу отменяемой, а не осиротевшей.
Источники
- Microsoft Learn, Resolve errors and warnings that involve async, await and the task-asynchronous protocol (C# reference) (точный текст
CS4014и указание ожидать через await или явно отбросить через_ =). - Microsoft Learn, Asynchronous programming with async and await (как выполняется возвращающий Task async-метод и где перехватываются исключения).
- Microsoft Learn, Task.WhenAll method (завершение, когда все ожидаемые задачи закончены, и повторный выброс агрегированных сбоев).
- Microsoft Learn, CA2012: Use ValueTasks correctly (анализатор, который ловит ненаблюдаемые awaitable, пропускаемые предупреждением компилятора).
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.