JavaRush /Курсы /C# SELF /Сводная стратегия обработки ошибок:

Сводная стратегия обработки ошибок: Task, async/ await, AggregateException

C# SELF
61 уровень , 4 лекция
Открыта

1. Введение

Писать программы, которые никогда не падают – всё равно что верить, будто баги боятся вашего IDE. На самом деле, чем сложнее становится код (особенно в многопоточной и асинхронной среде), тем изобретательнее становятся баги и тем изощрённее — ошибки. Игнорируя исключения в потоках и задачах, легко получить утечки ресурсов, зависания, потерю данных или неожиданные сбои спустя часы после запуска.

В этой лекции мы соберём воедино лучшие практики обработки ошибок в многопоточном и асинхронном программировании на C#. Вы узнаете, как грамотно ловить исключения, как разбирать «кучу» одновременных ошибок (в стиле AggregateException), что делать с задачами, запущенными «в молоко», и почему игнорировать исключения — опасно и бессмысленно.

Почему всё не так просто

  • Поток, в котором возникает ошибка, может отличаться от потока, где расположен try-catch.
  • Асинхронные задачи не выбрасывают исключения сразу — они «упаковываются» и ждут обработки при await (или синхронном ожидании).
  • При одновременной работе с несколькими задачами (например, Task.WhenAll) может возникнуть несколько ошибок — их надо учитывать.
  • Операции вида fire-and-forget могут полностью «потерять» исключение без явного обработчика.

Эта особенность — разделение контекста выполнения. Представьте программу как цирк с несколькими аренами: если на одной случился пожар, его не сразу видно на других. Важно уметь правильно отслеживать и гасить такие «пожары».

2. Исключения в задачах: Task и Task<TResult>

Как задачи сигнализируют об ошибках

Когда в задаче происходит необработанное исключение, оно не «вылетает» сразу наружу. Задача становится Faulted, а исключение сохраняется внутри. Получить его можно:

  • Ожидая завершение с помощью await (или через task.Wait()/task.Result — но лучше так не делать);
  • Проверив свойство task.Exception — там будет AggregateException.

Пример


// Асинхронный метод с ошибкой
async Task FailAsync()
{
    await Task.Delay(100);
    throw new InvalidOperationException("Что-то пошло не так");
}

async Task MainAsync()
{
    try
    {
        await FailAsync();
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Ошибка: {ex.Message}");
    }
}

Если не поставить try-catch, программа упадёт. Если поставить — исключение поймается корректно, даже если ошибка возникла в другой задаче.

AggregateException: ошибки оптом

При await Task.WhenAll(tasks) ошибки из нескольких задач собираются в один AggregateException (в его InnerExceptions).


async Task MultiFailAsync()
{
    Task t1 = Task.Run(() => throw new InvalidOperationException("Ошибка 1"));
    Task t2 = Task.Run(() => throw new ArgumentException("Ошибка 2"));
    try
    {
        await Task.WhenAll(t1, t2);
    }
    catch (Exception ex)
    {
        if (ex is AggregateException agg)
        {
            foreach (var e in agg.InnerExceptions)
                Console.WriteLine($"Исключение: {e.Message}");
        }
        else
        {
            Console.WriteLine($"Одиночная ошибка: {ex.Message}");
        }
    }
}

Внимание: при использовании await Task.WhenAll(tasks) .NET «разворачивает» AggregateException, и в catch вы получите «первое» исключение. Полный список доступен через task.Exception.InnerExceptions, если задача завершилась с ошибкой.

3. «Fire and Forget»: почему нельзя просто забыть про задачу

«Запусти и забудь» часто ведёт к скрытым сбоям. Пример:


Task.Run(() => { throw new Exception("Бах!"); }); // Ошибка "улетает" в пустоту.

Современный рантайм хранит ошибку в задаче и может завершить процесс из‑за необработанного исключения. Лучший способ — сохранять ссылку на задачу и/или подписываться на событие TaskScheduler.UnobservedTaskException.

Как правильно?

  • Сохраняйте задачу, чтобы дождаться её завершения и обработать ошибку;
  • Для fire‑and‑forget поставьте обработчик прямо внутри делегата.

Task.Run(() => {
    try 
    {
        // Код, который может бросить исключение
    }
    catch (Exception ex)
    {
        // Логируем, уведомляем, но не выпускаем ошибку наружу
        Console.WriteLine($"Ошибка в fire-and-forget: {ex.Message}");
    }
});

4. Ошибки в потоках (Thread): «ловить нечего?»

Исключение в новом Thread нельзя поймать try-catch снаружи — только внутри тела потока.


var thread = new Thread(() =>
{
    try
    {
        throw new Exception("Ошибка в потоке");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Поймали ошибку внутри потока: {ex.Message}");
    }
});
thread.Start();

Если не поставить обработчик, исключение завершит только этот поток (если он фоновый: thread.IsBackground = true). Для нефоновых потоков необработанное исключение может завершить весь процесс. Всегда ставьте try-catch внутри потока.

Как возвращать результат и ошибки из потока?

  • Используйте очереди/коллекции для передачи результатов/ошибок;
  • Событийную модель;
  • Лучше переходите на задачи — с ними удобнее обрабатывать ошибки.

5. Параллельные циклы: ловим ошибки по‑особому

В параллельных циклах ошибки из разных веток агрегируются в AggregateException.


try
{
    Parallel.For(0, 5, i =>
    {
        if (i % 2 == 0)
            throw new Exception($"Ошибка в итерации {i}");
    });
}
catch (AggregateException ex)
{
    foreach (var e in ex.InnerExceptions)
        Console.WriteLine($"[Параллельный цикл] Ошибка: {e.Message}");
}

Если нужно логировать локальные сбои и продолжать остальные ветки, ставьте внутренние try-catch в каждой ветке.

6. Обработка ошибок при отмене задач

При отмене через CancellationToken по соглашению выбрасывается OperationCanceledException — это не сбой, а штатная остановка.


async Task DoWorkAsync(CancellationToken token)
{
    for (int i = 0; i < 10; i++)
    {
        token.ThrowIfCancellationRequested();
        await Task.Delay(100);
    }
}

// Где-то в коде:
var cts = new CancellationTokenSource();
var task = DoWorkAsync(cts.Token);
cts.Cancel(); // Примерно через 200 мс

try
{
    await task;
}
catch (OperationCanceledException)
{
    Console.WriteLine("Операция была отменена!");
}

Помимо ThrowIfCancellationRequested(), множество методов, поддерживающих токен (например, Task.Delay, HttpClient.GetAsync), сами выбросят OperationCanceledException. Проверяйте поддержку отмены.

7. Полезные нюансы

Подходы для всех случаев

  • Ставьте try-catch на верхнем уровне асинхронных методов — так вы поймаете «убежавшие» ошибки.
  • Не игнорируйте задачи: сохраняйте ссылки, дожидайтесь завершения, логируйте ошибки.
  • Для параллельных операций (Task.WhenAll / Parallel.ForEach) учитывайте AggregateException.
  • Отделяйте отмену от сбоев: ловите OperationCanceledException отдельно.
  • Логируйте ошибки, особенно «тихие», не валящие приложение.
  • Сохраняйте детали: логируйте все вложенные исключения, а не только первое.
  • Обрабатывайте ошибки «на месте»: если не знаете, что делать — хотя бы залогируйте.

Где ловить ошибку в многопоточном и асинхронном коде

flowchart TD
    A[Главный поток / UI] -->|Запускает задачу| B[Task/async]
    B -->|Внутри задача| C[try/catch внутри асинхронного метода]
    B -->|await в главном потоке| D[try/catch вокруг await]
    A -->|Запускает Thread| E[Thread]
    E -->|Внутри| F[try/catch внутри потока]
    B -->|Много задач| G[Task.WhenAll / Parallel.ForEach]
    G -->|Ошибка| H[AggregateException]

8. Типичные ошибки и «грабли»

Ошибка №1: ожидание задачи через .Result или .Wait(). Возможен deadlock и/или неожиданный AggregateException.

Ошибка №2: запуск fire‑and‑forget без внутреннего try-catch — задача может тихо упасть, диагностики не будет.

Ошибка №3: пропущенные ошибки в параллельных циклах — часть работы не выполнена, а вы этого не замечаете.

Ошибка №4: неразличение отмены (OperationCanceledException) и реальных ошибок.

Ошибка №5: логирование только первой ошибки среди нескольких задач — остальные остаются «в тени».

Ошибка №6: переиспользование одного объекта Exception для всех задач — у каждой ошибки должен быть свой экземпляр.

Ошибка №7: отсутствие обработки исключений в UI‑потоке — фоновая ошибка остаётся незамеченной, интерфейс ведёт себя «призрачно».

2
Задача
C# SELF, 61 уровень, 4 лекция
Недоступна
Обработка исключений в асинхронной задаче
Обработка исключений в асинхронной задаче
1
Опрос
Исключения в классических Thread, 61 уровень, 4 лекция
Недоступен
Исключения в классических Thread
Обработка ошибок в асинхронном коде
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ