Обработка ошибок в асинхронных методах
Обработка ошибок в асинхронных методах, использующих ключевые слова async и await, имеет свои особенности.
Для обработки ошибок выражение await помещается в блок try:
try
{
await PrintAsync("Hello METANIT.COM");
await PrintAsync("Hi");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
async Task PrintAsync(string message)
{
// если длина строки меньше 3 символов, генерируем исключение
if (message.Length < 3)
throw new ArgumentException($"Invalid string length: {message.Length}");
await Task.Delay(100); // имитация продолжительной операции
Console.WriteLine(message);
}
В данном случае асинхронный метод PrintAsync генерирует исключение ArgumentException, если методу передается строка с длиной меньше 3 символов.
Для обработки исключения в методе Main выражение await помещено в блок try. В итоге при выполнении вызова await PrintAsync("Hi") будет сгенерировано исключение, что привет к генерации исключения. Однако программа не остановит аварийно свою работу, а обработает исключение и продолжит дальнейшие вычисления.
Консольный вывод программы:
Hello METANIT.COM
Invalid string length: 2
Следует учитывать, что если асинхронный метод имеет тип void, то в этом случае исключение во вне не передается, соответственно мы не сможем обработать исключение при вызове метода:
try
{
PrintAsync("Hello METANIT.COM");
PrintAsync("Hi"); // здесь программа сгенерирует исключение и аварийно остановится
await Task.Delay(1000); // ждем завершения задач
}
catch (Exception ex) // исключение НЕ будет обработано
{
Console.WriteLine(ex.Message);
}
async void PrintAsync(string message)
{
// если длина строки меньше 3 символов, генерируем исключение
if (message.Length < 3)
throw new ArgumentException($"Invalid string length: {message.Length}");
await Task.Delay(100); // имитация продолжительной операции
Console.WriteLine(message);
}
В данном случае, не смотря на то, что асинхронные методы вызываются в блоке try, исключение не будет перехвачено и обработано. В этом один из минусов применения асинхронных void-методов. Правда, в этом случае мы можем определить обработку исключения в самом асинхронном методе:
PrintAsync("Hello METANIT.COM");
PrintAsync("Hi");
await Task.Delay(1000); // ждем завершения задач
async void PrintAsync(string message)
{
try
{
// если длина строки меньше 3 символов, генерируем исключение
if (message.Length < 3)
throw new ArgumentException($"Invalid string length: {message.Length}");
await Task.Delay(100); // имитация продолжительной операции
Console.WriteLine(message);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Исследование исключения
При возникновении ошибки у объекта Task, представляющего асинхронную задачу, в которой произошла ошибка, свойство IsFaulted имеет значение true. Кроме того, свойство Exception объекта Task содержит всю информацию об ошибке. Проинспектируем данное свойство:
var task = PrintAsync("Hi");
try
{
await task;
}
catch
{
Console.WriteLine(task.Exception?.InnerException?.Message); // Invalid string length: 2
Console.WriteLine($"IsFaulted: {task.IsFaulted}"); // IsFaulted: True
Console.WriteLine($"Status: {task.Status}"); // Status: Faulted
}
async Task PrintAsync(string message)
{
// если длина строки меньше 3 символов, генерируем исключение
if (message.Length < 3)
throw new ArgumentException($"Invalid string length: {message.Length}");
await Task.Delay(1000); // имитация продолжительной операции
Console.WriteLine(message);
}
И если мы передадим в метод строку с длиной меньше 3 символов, то task.IsFaulted будет равно true.
Обработка нескольких исключений. WhenAll
Если мы ожидаем выполнения сразу нескольких задач, например, с помощью Task.WhenAll, то мы можем получить сразу несколько исключений одномоментно для каждой выполняемой задачи. В этом случае мы можем получить все исключения из свойства Exception.InnerExceptions:
// определяем и запускаем задачи
var task1 = PrintAsync("H");
var task2 = PrintAsync("Hi");
var allTasks = Task.WhenAll(task1, task2);
try
{
await allTasks;
}
catch (Exception ex)
{
Console.WriteLine($"Exception: {ex.Message}");
Console.WriteLine($"IsFaulted: {allTasks.IsFaulted}");
if(allTasks.Exception is not null)
{
foreach (var exception in allTasks.Exception.InnerExceptions)
{
Console.WriteLine($"InnerException: {exception.Message}");
}
}
}
async Task PrintAsync(string message)
{
// если длина строки меньше 3 символов, генерируем исключение
if (message.Length < 3)
throw new ArgumentException($"Invalid string: {message}");
await Task.Delay(1000); // имитация продолжительной операции
Console.WriteLine(message);
}
Здесь в два вызова метода PrintAsync передаются заведомо некорректные значения. Таким образом, при обоих вызовах будет сгенерирована ошибка.
Хотя блок catch через переменную Exception ex будет получать одно перехваченное исключение, но с помощью коллекции Exception.InnerExceptions мы сможем получить информацию обо всех возникших исключениях.
В итоге при выполнении этого метода мы получим следующий консольный вывод:
Exception: Invalid string: H
IsFaulted: True
InnerException: Invalid string: H
InnerException: Invalid string: Hi