Как правильно реализовать обработку исключений в C#?

«Как правильно реализовать обработку исключений в C#?» — вопрос из категории C# Core, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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

1. Базовый синтаксис и рекомендации:

try
{
    // Код, который может выбросить исключение
    var data = File.ReadAllText("config.json");
    var config = JsonSerializer.Deserialize<Config>(data);
}
catch (FileNotFoundException ex) // Ловите конкретные исключения
{
    _logger.LogWarning(ex, "Файл конфигурации не найден, используются значения по умолчанию.");
    // Восстановление: создаем конфиг по умолчанию
    config = Config.Default;
}
catch (JsonException ex) // Обработка ошибок десериализации
{
    _logger.LogError(ex, "Неверный формат JSON в конфигурационном файле.");
    // Невосстановимая ошибка — пробрасываем выше
    throw new InvalidOperationException("Ошибка загрузки конфигурации", ex);
}
finally
{
    // Код для освобождения ресурсов (файлов, сетевых подключений)
    // Выполняется ВСЕГДА, даже если было исключение или return.
}

2. Ключевые принципы:

  • Не ловите то, что не можете обработать: Пусть исключение всплывает до уровня, где есть достаточно контекста для его корректоной обработки (например, контроллер API вернет 500 Internal Server Error).
  • Используйте конкретные типы исключений в catch: catch (Exception) должен быть крайней мерой, обычно на верхнем уровне приложения (например, в middleware).
  • Всегда логируйте исключения: Используйте ILogger, передавая само исключение как параметр, чтобы сохранить стектрейс.
  • Избегайте «пустых» catch-блоков (catch {}): Это скрывает ошибки и усложняет отладку.

3. Создание пользовательских исключений: Создавайте их для ошибок предметной области (business logic).

public class InsufficientFundsException : Exception
{
    public decimal CurrentBalance { get; }
    public decimal RequiredAmount { get; }

    public InsufficientFundsException(decimal currentBalance, decimal requiredAmount)
        : base($"Недостаточно средств. Текущий баланс: {currentBalance}, требуется: {requiredAmount}")
    {
        CurrentBalance = currentBalance;
        RequiredAmount = requiredAmount;
    }
}
// Использование:
throw new InsufficientFundsException(balance, amount);

4. Глобальная обработка в ASP.NET Core: Используйте middleware для централизованной обработки.

// В Program.cs
app.UseExceptionHandler(appError =>
{
    appError.Run(async context =>
    {
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        context.Response.ContentType = "application/json";
        var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
        // Логируем исключение
        _logger.LogError(exception, "Необработанное исключение");
        // Возвращаем клиенту безопасный ответ (без деталей в продакшене)
        await context.Response.WriteAsJsonAsync(new { error = "Произошла внутренняя ошибка." });
    });
});

5. Исключения и асинхронный код: В асинхронных методах исключения «заворачиваются» в AggregateException при использовании Task.Wait() или Task.Result. Всегда используйте await, который пробрасывает исходное исключение.

// ПЛОХО: Получите AggregateException
Task.Run(() => throw new InvalidOperationException()).Wait();

// ХОРОШО: Получите InvalidOperationException
await Task.Run(() => throw new InvalidOperationException());