Как правильно реализовать обработку исключений в 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());

Ответ 18+ 🔞

А, обработка исключений! Ну это ж классика, вечная тема, как эти ваши крестьяне в Палестинах. Все думают: «О, 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
{
    // А это святое. Этот кусок выполнится ВСЕГДА. Даже если всё ебнулось к чертям.
    // Закрывай файлы, рви соединения, делай что должен.
}

2. Главные принципы, которые нарушают все, а потом плачут.

  • Не лови то, что не можешь переварить. Ну серьёзно, зачем тебе ловить OutOfMemoryException в методе расчета скидки? Пусть летит наверх, там, в контроллере, его поймают и отдадут клиенту внятную ошибку 500, а не просто сдохнет поток.
  • Лови по имени, а не просто Exception. catch (Exception) — это как кричать «Муму!» на всё, что движется. Это для самого верхнего уровня, где ты уже просто логируешь и говоришь «всё пропало».
  • Логируй, сука, всегда! И передавай само исключение в логгер, а не просто строку. Иначе потом будешь как дурак смотреть в логи и гадать, откуда ноги растут.
  • Пустой catch {} — это прямой путь в ад. Это когда ты взял и закопал ошибку в землю. Через полгода продакшн падает, а ты нихуя не понимаешь почему. Пиздец.

3. Свои исключения — для своих косяков. Допустим, у тебя логика перевода денег. «Недостаточно средств» — это же не системная ошибка, а бизнес-правило. Вот и создай своё.

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 — делай по-взрослому. Не размазывай try-catch по каждому контроллеру. Сделай один жирный 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.Run(() => throw new InvalidOperationException()).Wait();

// ХОРОШО, ДЕЛАЙ ТАК: await развернёт тебе оригинальное исключение, как есть. Красота.
await Task.Run(() => throw new InvalidOperationException());

Короче, смысл в чём: исключения — это не враги, это система оповещения. Твоя задача — не заткнуть все сирены скотчем, а правильно на них реагировать. Одни — просто предупреждения, на другие — надо нажать большую красную кнопку. Главное — понимать, что происходит, а не просто бездумно обкладываться try-catch, как одеялом. Иначе будет тебе та же трагедия — придётся свою же «Муму» топить, потому что вовремя не разобрался.