Обработка исключений в асинхронных методах

Асинхронные методы в 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 сработал:

System.DivideByZeroException: Делитель равен нулю! at ConsoleApp1.Program.Devide(Single a, Single b) in …. at ConsoleApp1.Program.Main(String[] args) in …..

здесь, на первый взгляд, всё работает также, как и в синхронных методах, но, на самом деле, когда перехватывается сообщение об ошибке в асинхронном методе, то выполнение кода не прерывается и идет дальше до момент пока мы не попробуем получить результат с использованием оператора 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();
}

Теперь запустим приложение и увидим следующее:

Выполнили ошибочное деление System.DivideByZeroException: Делитель равен нулю! at ConsoleApp1.Program.Devide(Single a, Single b) in …..

Если запустить пошаговую отладку, то увидим следующее поведение:

  1. Доходим до строки: var res = Devide(4, 0);
  2. Заходим в метод Devide
  3. Генерируем исключение
  4. Выходим из метода Devide обратно в Main
  5. Выводим в консоль строку
  6. Снова заходи в Devide на строке var res2 = Devide(4, 2);
  7. И на строке с оператором 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.

Unhandled exception. Выполнили ошибочное деление
System.DivideByZeroException: Делитель равен нулю!
at ConsoleApp1.Program.Devide(Single a, Single b) in …

2

Где хранятся исключения асинхронных методов

Когда асинхронный метод генерирует исключение, то оно сохраняется в свойстве 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. В консоли мы увидим следующее:

Faulted
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. В консоли увидим следующее

Faulted
Исключение: Делитель равен нулю! Источник: ConsoleApp1
Исключение: Делитель равен нулю! Источник: ConsoleApp1
Исключение: Делитель равен нулю! Источник: ConsoleApp1

Итого

Обработка исключений в асинхронных методах осуществляется также с использованием блоков try...catch, однако, при этом во внешнем коде исключение будет сгенерировано только в момент ожидания результата задачи (в месте использования оператора await). При этом, объект типа Task хранит сведения об исключении в свойстве Task.Exception. Если в нескольких ожидаемых асинхронных методах генерируются исключения, то все эти исключения попадают в свойство Task.Exception.InnerExceptions.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии