Дан код на C#: async void Save(){ await db.SaveAsync(); throw new Exception("fail"); } — объясните проблему использования async void, как правильно обрабатывать исключения в асинхронных методах и какие паттерны для resiliency применять
Проблема - `async void` нельзя дождаться и нельзя корректно обработать исключение у вызывающего: исключение будет брошено в контекст синхронизации (или в пул задач) и не попадёт в `await` у вызывающего — часто приводит к непредсказуемому завершению приложения или «потерянным» ошибкам. `async void` допустим только для обработчиков событий (event handlers). Как правильно — обработка исключений 1. Меняйте сигнатуру на `async Task` (или `Task`), чтобы исключения корректно проксировались и могли быть пойманы: ``` async Task SaveAsync() { await db.SaveAsync(); throw new Exception("fail"); } // у вызывающего: try { await SaveAsync(); } catch (Exception ex) { // обработка/логирование } ``` 2. Если нужен fire-and-forget — не используйте `async void`. Делайте: - запуск в фоне с сохранением и обработкой ошибки: ``` _ = SaveAsync().ContinueWith(t => Log(t.Exception), TaskContinuationOptions.OnlyOnFaulted); ``` - или очередь фоновых задач / IHostedService / BackgroundService / внешняя очередь (Hangfire, Azure Queue) — это надёжнее. 3. Для обработчиков событий допускается `async void`, но обязательно локальный `try/catch`: ``` async void Button_Click(object s, EventArgs e) { try { await SaveAsync(); } catch (Exception ex) { // показать ошибку пользователю/залогировать } } ``` 4. Всегда поддерживайте `CancellationToken` для отмены и используйте `ConfigureAwait(false)` в библиотечных методах при необходимости. Resiliency — шаблоны и практики - Retry (повторы) с экспоненциальной задержкой: - пример с Polly: ``` var retry = Policy .Handle() .WaitAndRetryAsync(new[] { TimeSpan.FromSeconds(111), TimeSpan.FromSeconds(222), TimeSpan.FromSeconds(444) }, (ex, ts) => Log($"retry after {ts} due to {ex}")); await retry.ExecuteAsync(() => db.SaveAsync()); ``` - Circuit Breaker — разрыв при повторных ошибках, чтобы не перегружать зависимость: ``` var cb = Policy .Handle() .CircuitBreakerAsync(555, TimeSpan.FromSeconds(303030)); ``` - Timeout — ограничить время ожидания операции: ``` var timeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(101010)); ``` - Bulkhead (изоляция) — ограничение параллельных вызовов для защиты ресурсов. - Fallback — запасной путь (например, записать в локальную очередь/кеш, если основной save упал). - Комбинация политик (wrap): retry + circuit breaker + timeout: ``` var policy = Policy.WrapAsync(retry, cb, timeout); await policy.ExecuteAsync(() => db.SaveAsync()); ``` - Метрики и логирование — счётчики ошибок, время ответа, трейсинг. - Надёжные фоновые обработчики — для fire-and-forget используйте очередь + длительно работающий воркер (IHostedService), где ошибки контролируются и повторяются безопасно. Короткие рекомендации - Избегайте `async void` (кроме событий). Используйте `Task`/`Task`. - Обрабатывайте исключения либо внутри метода (если локально нужно), либо на уровне вызывающего (`try/await`). - Для сетевых/внешних операций применяйте retry, circuit breaker, timeout, bulkhead и fallback (можно взять Polly). - Для фоновых задач используйте надёжную очередь + воркер вместо fire-and-forget. Это позволит корректно ловить ошибки, делать повторы и защищать приложение от лавинообразных отказов.
- `async void` нельзя дождаться и нельзя корректно обработать исключение у вызывающего: исключение будет брошено в контекст синхронизации (или в пул задач) и не попадёт в `await` у вызывающего — часто приводит к непредсказуемому завершению приложения или «потерянным» ошибкам. `async void` допустим только для обработчиков событий (event handlers).
Как правильно — обработка исключений
1. Меняйте сигнатуру на `async Task` (или `Task`), чтобы исключения корректно проксировались и могли быть пойманы:
```
async Task SaveAsync()
{
await db.SaveAsync();
throw new Exception("fail");
}
// у вызывающего:
try
{
await SaveAsync();
}
catch (Exception ex)
{
// обработка/логирование
}
```
2. Если нужен fire-and-forget — не используйте `async void`. Делайте:
- запуск в фоне с сохранением и обработкой ошибки:
```
_ = SaveAsync().ContinueWith(t => Log(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
```
- или очередь фоновых задач / IHostedService / BackgroundService / внешняя очередь (Hangfire, Azure Queue) — это надёжнее.
3. Для обработчиков событий допускается `async void`, но обязательно локальный `try/catch`:
```
async void Button_Click(object s, EventArgs e)
{
try
{
await SaveAsync();
}
catch (Exception ex)
{
// показать ошибку пользователю/залогировать
}
}
```
4. Всегда поддерживайте `CancellationToken` для отмены и используйте `ConfigureAwait(false)` в библиотечных методах при необходимости.
Resiliency — шаблоны и практики
- Retry (повторы) с экспоненциальной задержкой:
- пример с Polly:
```
var retry = Policy
.Handle()
.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(111),
TimeSpan.FromSeconds(222),
TimeSpan.FromSeconds(444)
}, (ex, ts) => Log($"retry after {ts} due to {ex}"));
await retry.ExecuteAsync(() => db.SaveAsync());
```
- Circuit Breaker — разрыв при повторных ошибках, чтобы не перегружать зависимость:
```
var cb = Policy
.Handle()
.CircuitBreakerAsync(555, TimeSpan.FromSeconds(303030));
```
- Timeout — ограничить время ожидания операции:
```
var timeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(101010));
```
- Bulkhead (изоляция) — ограничение параллельных вызовов для защиты ресурсов.
- Fallback — запасной путь (например, записать в локальную очередь/кеш, если основной save упал).
- Комбинация политик (wrap): retry + circuit breaker + timeout:
```
var policy = Policy.WrapAsync(retry, cb, timeout);
await policy.ExecuteAsync(() => db.SaveAsync());
```
- Метрики и логирование — счётчики ошибок, время ответа, трейсинг.
- Надёжные фоновые обработчики — для fire-and-forget используйте очередь + длительно работающий воркер (IHostedService), где ошибки контролируются и повторяются безопасно.
Короткие рекомендации
- Избегайте `async void` (кроме событий). Используйте `Task`/`Task`.
- Обрабатывайте исключения либо внутри метода (если локально нужно), либо на уровне вызывающего (`try/await`).
- Для сетевых/внешних операций применяйте retry, circuit breaker, timeout, bulkhead и fallback (можно взять Polly).
- Для фоновых задач используйте надёжную очередь + воркер вместо fire-and-forget.
Это позволит корректно ловить ошибки, делать повторы и защищать приложение от лавинообразных отказов.