Содержание
Асинхронные методы в 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
.