Содержание
Асинхронные методы в C# также, как и другие методы могут генерировать исключения. Например, никто не даёт гарантии, что сервер во время ответит на ваш асинхронный запрос. Или же пользователь передаст в приложение неверные данные, например, попытается поделить число на ноль — получим ошибку. В этом случае вам может потребоваться обработка исключений в асинхронном методе.
Обработка исключений в асинхронных методах
Обработку исключений в асинхронных методах можно осуществлять также, с использованием блоков try...catch, как и в обычных методах. Но, как говориться, есть нюансы. Рассмотрим такой пример:
internal class Program
{
static async Task<float> Devide(float a, float b)
{
await Task.Delay(100);
if (b == 0)
throw new DivideByZeroException("Делитель равен нулю!");
return a / b;
}
static async Task Main(string[] args)
{
try
{
Console.WriteLine(await Devide(4, 0));
Console.WriteLine(await Devide(4, 2));
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
}
В первом ожидании результата от метода Devide мы должны получить ошибку — деление на ноль. Запустим приложение и убедимся, что блок catch сработал:
здесь, на первый взгляд, всё работает также, как и в синхронных методах, но, на самом деле, когда перехватывается сообщение об ошибке в асинхронном методе, то выполнение кода не прерывается и идет дальше до момент пока мы не попробуем получить результат с использованием оператора await. Убедиться в этом просто — перепишем немного наш метод:
static async Task Main(string[] args)
{
try
{
var res = Devide(4, 0); //ТУТ ОШИБКА
Console.WriteLine("Выполнили ошибочное деление"); //убедимся, что выполнение других операций не прервалось
var res2 = Devide(4, 2);
Console.WriteLine(await res);
Console.WriteLine(await res2);
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
Console.ReadKey();
}
Теперь запустим приложение и увидим следующее:
Если запустить пошаговую отладку, то увидим следующее поведение:
- Доходим до строки:
var res = Devide(4, 0); - Заходим в метод
Devide - Генерируем исключение
- Выходим из метода
Devideобратно вMain - Выводим в консоль строку
- Снова заходи в Devide на строке
var res2 = Devide(4, 2); - И на строке с оператором await:
Console.WriteLine(await res);наше приложение ловит исключение и выводит его в консоль.
Второй момент, связанный с обработкой исключений в асинхронных методах связан с типом void. Если асинхронный метод ничего не возвращает, то исключение не передается вовне. Опять же, убедимся в этом на примере. Перепишем наш метод Devide следующим образом:
static async void Devide(float a, float b)
{
if (b == 0)
throw new DivideByZeroException("Делитель равен нулю!");
await Task.Delay(100);
Console.WriteLine(a / b);
}
Так как асинхронный метод с void ожидать нельзя, то метод Main станет таким:
static async Task Main(string[] args)
{
try
{
Devide(4, 0); //ТУТ ОШИБКА
Console.WriteLine("Выполнили ошибочное деление");
Devide(4, 2);
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
Console.ReadKey();
}
Теперь запустите приложение без отладки (прямо из папки bin/Debug) и убедитесь, что исключение не было отловлено в блоке catch, а вывелось в консоль как Unhandled exception.
Где хранятся исключения асинхронных методов
Когда асинхронный метод генерирует исключение, то оно сохраняется в свойстве Task.Exception имеющим тип AggregateException, а сама задача получает статус Faulted. Рассмотрим свойство Task.Exception более подробно:
static async Task Main(string[] args)
{
Task error = Devide(4, 0);
try
{
await error;
}
catch
{
Console.WriteLine(error?.Status);
Console.WriteLine(error?.Exception?.Message);
Console.WriteLine(error?.Exception?.InnerException?.Message);
}
Console.ReadKey();
}
Здесь метод Devide возвращает объект типа Task. В консоли мы увидим следующее:
One or more errors occurred. (Делитель равен нулю!)
Делитель равен нулю!
Наше исключение DivideByZeroException попало в свойство InnerException у Task.Exception. Дело в том, что асинхронные методы могут сгенерировать не одно, а сразу несколько исключений, например, когда мы пользуемся методом WhenAll и, чтобы иметь доступ к этим исключениям и был создан такой механизм их обработки в асинхронных методах. Вот, например, как мы можем получить сразу несколько исключений и обработать их в своем приложении:
Task error = Devide(4, 0);
Task error2 = Devide(5, 0);
Task error3 = Devide(6, 0);
var data = Task.WhenAll(error, error2, error3);
try
{
await data;
}
catch
{
Console.WriteLine(data?.Status);
if (data?.Exception?.InnerExceptions.Count > 0)
{
foreach (Exception ex in data.Exception.InnerExceptions)
{
Console.WriteLine($"Исключение: {ex.Message} Источник: {ex.Source}");
}
}
}
И все они хранятся в свойстве Task.Exception.InnerExceptions. В консоли увидим следующее
Исключение: Делитель равен нулю! Источник: ConsoleApp1
Исключение: Делитель равен нулю! Источник: ConsoleApp1
Исключение: Делитель равен нулю! Источник: ConsoleApp1
Итого
Обработка исключений в асинхронных методах осуществляется также с использованием блоков try...catch, однако, при этом во внешнем коде исключение будет сгенерировано только в момент ожидания результата задачи (в месте использования оператора await). При этом, объект типа Task хранит сведения об исключении в свойстве Task.Exception. Если в нескольких ожидаемых асинхронных методах генерируются исключения, то все эти исключения попадают в свойство Task.Exception.InnerExceptions.