Помните ли вы о существовании goto
?
Все его ненавидят, не исключая того, что найдётся пара случаев, когда он действительно упростит жизнь. Например, выход и множества вложенных циклов,. Оставим за скобками то, почему вообще это произошло.
Но почему-то я не встречал никакого негатива насчёт throw
. А ведь это точно такая же фигня, если даже не хуже.

В моих личных проектах я их использую только тогда, когда там программа действительно должна ругнуться, например, на отсутствие обязательного файла или обязательного параметра атрибута. Когда программа даже не должна запускаться без этого или пользователь в лице разработчика сделал так, как не должно быть.
Во всех случаях, где мне надо вернуть ошибку, я использую паттерн Result. У меня всегда возвращаются или данные, или исключение.
Если же исключение случается, то это действительно где-то ошибка, которую надо исправить. На этот случай есть глобальный перехватчик.
Минимум кода. Максимум удовольствия.
Выше скорость выполнения кода (try-catch
работает медленнее).
Логика линейная и предсказуемая.
Что происходит, когда мы решаем, что выбрасывание исключения — отличная идея? Выбросил, где надо, и забыл про остальную часть кода. Где-то там поймал — и всё. Это имеет право на существование (но лучше, всё же, нет), если у вас не глубокая вложенность и какая-то простая API-шечка, у которой есть контроллеры, сервисы и репозитории — не потеряешься.
Но довелось мне работать на проекте с легаси, где вложенность превышает разумные пределы. Конечно, тогда ещё, возможно, не знали о возможности возврата ошибки вместо её выбрасывания, а возврат сразу двух значений выглядел не очень красиво:var result = new Tuple<DataType, string>(data, error);
В итоге получилось так, что вызывается некий метод, далее всё это сильно ветвится, вызывая прочие методы. И разные методы выбрасывают разные типы исключений, а ещё некоторые из них отлавливаются и просто логируются, а некоторые пробрасываются дальше. Некоторые нигде не отлавливаются, а некоторые перебрасываются с новым текстом ошибки. В общем, работа с таким зоопарком исключений ничем не лучше, чем если бы туда напихали пачку goto
. Может, даже проще было.
И я был бы рад это всё придумать, но мне каждый раз приходится самостоятельно анализировать все вызовы методов один за другим, чтобы понять, увидит ли конечный пользователь выброшенное сообщение или нет.
Уверен, что если у вас нечто похожее, то вы сами не знаете, как работает ваш код и, скорей всего, вы иногда пишите try-catch
в коде "на всякий случай".
Моя идея проста: выбрасывайте исключения только там, где они действительно нужны. При бездумном использовании throw == goto
.
Если же какая-то логика должна привести к сообщению об ошибке, не поленитесь и верните его из вашего метода — и ваш код станет чище, понятнее, его логика будет линейной и непрерывной.
В моих проектах try-catch
можно по пальцам пересчитать. Естественно, есть глобальный. Есть в местах, где вызывается библиотечный метод, который может выкинуть исключение, если приходят неверные данные, а такая вероятность есть. Например, при вызове JsonSerializer.Deserialize(text);
, где text
— ответ от AI.
Вот пример того, как это можно сделать. Ниже всё, что нужно для реализации паттерна Result и даже чуть больше. Вы можете добавить что-то своё.
// когда хватило бы bool, но нужен ещё и текст ошибки
public class Result
{
public ResultException Exception { get; set; }
[JsonIgnore]
public virtual bool IsSuccessful => Exception is null;
[JsonConstructor]
public Result() { }
public Result(ResultException exception) => Exception = exception;
public Result(ResultExceptionType exceptionType, string message = null, string additional = null) =>
Exception = new ResultException(exceptionType, message, additional);
public Result(Exception exception, ResultExceptionType exceptionType, string message = null, string additional = null) =>
Exception = new ResultException(exception, exceptionType, message, additional);
public static Result<T> ToResult<T>() =>
IsSuccessful
? new Result<T>(default(T))
: new Result<T>(Exception, Exception.ResultExceptionType, Exception.Message, Exception.Additional);
// вместо пустого результата
[JsonIgnore]
public static Result Empty => new();
}
// данные или ошибка
public class Result<T> : Result
{
public T? Data { get; set; }
public bool IsNull { get; set; } // иногда null - тоже результат
[JsonConstructor]
public Result() { }
public Result(T? data)
{
Data = data;
IsNull = Data == null;
}
public Result(ResultException exception) => Exception = exception;
public Result(ResultExceptionType exceptionType, string? message = null, string? additional = null) =>
Exception = new ResultException(exceptionType, message, additional);
public Result(Exception exception, ResultExceptionType exceptionType, string? message = null, string? additional = null) =>
Exception = new ResultException(exception, exceptionType, message, additional);
public Result(T data, ResultExceptionType exceptionType, string? message = null, string? additional = null)
{
if (data == null)
Exception = new ResultException(exceptionType, message, additional);
else Data = data;
}
// удобно для мапинга
public Result<TNew> NewResult<TNew>(Func<T, TNew> convertor) where TNew : new() =>
IsSuccessful ? new Result<TNew>(convertor(Data)) : new Result<TNew>(Exception);
[JsonIgnore]
public override bool IsSuccessful => (Data != null || IsNull) && Exception == null;
}
// тип исключения, чтобы потом можно было обработать
public enum ResultExceptionType
{
NotValid,
NotFound,
AlreadyExists,
...
}
// исключение
public class ResultException : Exception
{
public string? Additional { get; }
public ResultExceptionType ResultExceptionType { get; }
public ResultException() { }
public ResultException(ResultExceptionType resultExceptionType, string? message = null, string? additional = null) :
base(message)
{
Additional = additional;
ResultExceptionType = resultExceptionType;
}
public ResultException(Exception exception, ResultExceptionType resultExceptionType, string? message = null, string? additional = null) :
base(message, exception)
{
Additional = additional;
ResultExceptionType = resultExceptionType;
}
}
Как это используется:
// Было
public SomeModel Get(int id)
{
if (id < 0)
throw new Exception("Wrong id");
return repository.Get(id);
}
// Стало
public Result<SomeModel> Get(int id)
{
if (id < 0)
return new(ResultExceptionType.NotValid, "Wrong id"/*, "some_additional_data"*/)
return repository.Get(id); // если repository возвращает Result<SomeModel>
// return repository.Get(id).ToResult<SomeModel>(); // если repository возвращает Result
}
-----------------------
// Было
[HttpGet]
public IActionResult Get(int id)
{
try
{
var data = service.Get(id); // тот самый Get() из примера выше
return Ok(data); // возвращает SomeModel
}
catch (Exception e)
{
logger.Log(e, ...);
return BadRequest(e.Message); // возвращает {"Message": "error_message"}
}
}
// Стало
[HttpGet]
public IActionResult Get(int id)
{
var result = service.Get(id);
if (result.IsSuccessful)
return Ok(result); // или return Ok(result.Data);
if (result.Exception.Type == ResultExceptionType.NotFound)
return NotFound();
else
return BadRequest(result); // или return BadRequest(result.Exception)
}
// Типичный запрос на добавление записи у меня выглядит так
[HttpPost]
public async Task<IActionResult> Insert([FromBody] SomeModelDto dto)
{
if (!validator.Validate(dto, ModelState))
return BadRequest(ModelState);
return Ok(await service.Insert(dto)); // возвращает Result<SomeModelDto>
// на клиенте уже проверяется наличие данных или исключения
}
// Да, это не RESTful API, но что ты мне сделаешь? Это мой pet project - как хочу, так и пишу.
Смотрите, как красиво, легко читается и предсказуемо выполняется!
Конечно, не нужно использовать Result также бездумно и где попало.
Если, например, обращение к БД должно вернуть коллекцию объектов, то и возвращайте коллекцию. Но если же коллекция не должна быть пустой, иначе ошибка, то это идеальное место для Result.
Хорошо читаемого вам кода!