Помните ли вы о существовании 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.

Хорошо читаемого вам кода!

Комментарии (158)


  1. Vitimbo
    02.08.2025 16:39

    Result, это такой же рак, как и возврат ошибок к golang. К тому же, при использовании "паттерна" Result, мы все равно должны использовать try catch на случай непредвиденных ошибок и обмазывать весь код гошным if err = nill.

    И ещё, можно не обрабатывать ошибки в методах контроллера, а ловить их прямо в Middleware. Тогда, кроме вызова метода и возврата результата, там не будет вообще ничего.


    1. ProgerMan Автор
      02.08.2025 16:39

      В статье я написал, что try-catch используется, но только там, где это действительно необходимо.

      Конечно, middleware на случай непредвиденных ошибок есть. Но всё, что можно предвидеть, уходит в Result. Проблема middleware в том, что он одинаково ловит и "wrong id", которое нужно показать пользователю, и null reference со stack trace, которое не нужно показывать пользователю. В моём коде туда попадает только то, что не должно попадать на глаза пользователю для логирования, отправки уведомления об ошибке и показа пользователю заглушки.


      1. Vitimbo
        02.08.2025 16:39

        Мы просто все 'наши' ошибки наследуем от некоего exception base. Если ошибка не наша, то просто идёт в логи, если наша, то возвращаем код, в зависимости от ошибки. Валидация так вообще сама по себе работает от фреймворка, не знаю, зачем тут эксепшены кидать.


    1. mahairod
      02.08.2025 16:39

      ~~


    1. Dhwtj
      02.08.2025 16:39

      Result, это такой же рак, как и возврат ошибок к golang

      зависит от языка, в раст это весьма элегантно


      1. Kano
        02.08.2025 16:39

        Это потому что там озаботились операторами помогающими с этими типами работать, в отличии от golang, а в шарпах при наличии нормальных исключений такой паттерн становится вредным советом.


        1. danslapman
          02.08.2025 16:39

          В C# можно переиспользовать старый linq синтаксис для этого благодаря утиной типизации


          1. withkittens
            02.08.2025 16:39

            Имхо, это сносно работает только для несложных игрушечных примеров. При первых же сложностях вроде async/await читаемость сильно падает.


    1. goremukin
      02.08.2025 16:39

      мы все равно должны использовать try catch на случай непредвиденных ошибок

      Платформа уже имеет хорошие дефолты на случай непредвиденных ошибок: 500 код и залогировать в ILogger.

      и обмазывать весь код гошным if err = nill

      Не обязательно, есть иные подходы. Посмотрите на ютубе "Scott Wlaschin Railway oriented programming". Такое вполне реализуемо и в C#. Некоторые библиотеки предоставляющие result pattern уже имеют необходимые расширения из коробки.

      Проблема исключений в .net в том, что они не декларируются как в java. Вызывая тот или иной метод ты не можешь быть уверен какие исключения могут упасть. Да, иногда бывает документация, но это не гарантия. В случае result pattern ты знаешь какие ошибки могут вернуться. Более того ты обязан обработать каждый кейс при матчинге.


    1. MadeByFather
      02.08.2025 16:39

      И ещё, можно не обрабатывать ошибки в методах контроллера, а ловить их прямо в Middleware.

      И в итоге получаете магическую коробку, которая непонятно как работает и какой возможен результат, при этом она ломается, если вы её запускаете не в окружении "Подставить под каждую магическую коробку костыль"


    1. V1ruS1989
      02.08.2025 16:39

      Мб я не понял статью но вот и согласится хочется что это онкология все какая-то...

      У клександреску читал про похожие подходы и что делаю я....

      Методы ничего не возвращают кроме эксепшн, никаких бросков нет, принимают ссылки возвращают нулл и тп...

      Да, все превращается в проверки

      Но нет нигде никаких подстеков с тру...

      Канеша тоже изврат но блин вообще то бросить можно что угодно, даже резалт. Но как ответить на вопрос зачем так делать...


  1. Politura
    02.08.2025 16:39

    throw == goto.

    И что? break в цикле это тоже goto, но если знаешь, что дальнейшие итерации не имеют смысла, то зачем вообще их делать? С исключениями точно также - если знаешь, что дальнейшая обработка запроса не имеет смысла, то зачем ее делать, зачем всем по цепочке вызовов проверять битый результат, когда можно сразу прыгнуть в самый конец?

    Далее, ваш пример:

    // Было
    [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)
    }
    

    Зачем????? Зачем вы делаете обработку ошибок в КАЖДОМ API эндпойнте???? Сделайте единый обработчик ошибок, сделайте разные типы исключений под разные варианты бизнеслогики, типа ValidationExeption, NotFoundExeption и тд, и пусть этот единый обработчик ими занимается. А эндпойнты будут выглядеть так:

    [HttpGet]
    public IActionResult Get(int id)
    {
        return Ok(service.Get(id));  // тот самый Get() из примера выше
    }
    

    И лучше чтоб он был асинхронным.


    1. ProgerMan Автор
      02.08.2025 16:39

      Это просто примеры, чтобы не перегружать не важными словами и кучей ненужных для этого классов. Каждый волен делать так, как пожелает.

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

      Получается, что у каждого такого метода может быть несколько состояний: вернул результат, выбросил ValidationException, выбросил NotFoundException, выбросил FooBarException, и т.д. И каждый надо учесть. У Result зависит от подхода. Можно всегда Ok(result) возвращать, можно разбить на Ok и BadResult, можно сделать больше и вообще на клиенте обрабатывать.


      1. Politura
        02.08.2025 16:39

        Получается, что у каждого такого метода может быть несколько состояний: вернул результат, выбросил ValidationException, выбросил NotFoundException, выбросил FooBarException, и т.д. И каждый надо учесть. 

        Зачем? В подавляющем большинстве случаев вы вообще нигде try/catch не делаете, только в одном едином обработчике ошибок. Везде по коду вы только бросаете исключения.

        Но вложенность до вызова десятка методов один за другим и каждый может выбросить какое-то исключение, часть из которых логируется, часть игнорируется, часть выбрасывается дальше с новым текстом или даже новым типом, код превращается в ад.

        И вы предлагаете заменить один ад другим, зачем? Надо исправлять, и не портить. Если бросаем исключение - то логгируем, перед тем как бросить. Все исключения которые бросаем - правильного типа, а не просто один общий Exception. Перепроверить места где исключения ловятся и затем опять бросаются - в подавляющем большинстве случаев этого не надо, есть несколько редких кейсов, их оставить. В обработчике ошибок самого верхнего уровня, который ловит все исключения, логгировать обязательно со стектрейсом.

        Все, проблемы решены, код чистый, всегда можно понять где что случилось. Я это говорю как человек десяток лет проработавший с несколькими большими легаси системами, где вложенность два-три десятка вызовов это обыденность, а классы встречаются размером за десять тысяч строк. Без эксепшенов там было-бы можно повеситься. С правильными эксепшенами код чище и багхантинг достаточно легкий.


        1. mahairod
          02.08.2025 16:39

          В подавляющем большинстве случаев вы вообще нигде try/catch не делаете, только в одном едином обработчике ошибок. 

          Конвертация ошибок и их сообщений может быть при переходе из одного слоя в другой (как то fine grained -> coarse grained)


          1. MyraJKee
            02.08.2025 16:39

            И в этом тоже как-будто ничего плохого нет...


      1. mahairod
        02.08.2025 16:39

        у каждого такого метода может быть несколько состояний

        Состояний ровно 3: норма, исключение и ошибка программы/ВМ. Далее процессинг для нормы и для исключений.


    1. feruxmax
      02.08.2025 16:39

      а теперь представьте, что нужно генерировать OpenAPI, чтобы клиент понимал, какие коды возможны, а у вас все NotFound и пр. в едином обработчике.

      Result тут как ни как хорош. Плюс рекомендация не использовать исключения для не исключительных ситуаций (NotFound, Conflict в конкурентных системах уже становятся далеко не исключительными), скорей бы discriminated unions завезли (или как они там сейчас называются)


      1. withkittens
        02.08.2025 16:39

        а теперь представьте, что нужно генерировать OpenAPI, чтобы клиент понимал, какие коды возможны, а у вас все NotFound и пр. в едином обработчике.

        А в чём сложность? Если у вас minimal api, возвращайте из функции Results<> [1], например Results<Ok<Foo>, NotFound, Conflict>. Если MVC, навесьте ProducesResponseType [2].


        1. feruxmax
          02.08.2025 16:39

          сложность не в задани перечня возвращаемых значений в каждом endpoint-е, а в контроле соотвествия, что всё, что в едином обработчике возвращается, перечислено в каждом endpoint-е (при этом скорей всего там будет с десяток возможных вариантов, но каждый endpoint может вернуть только подмножество этих кодов)


    1. Spyman
      02.08.2025 16:39

      Вот я со всем с вами соглаен, кроме

      Сделайте единый обработчик ошибок

      Буквально в прошлом проекте был такой, и показал себя ужасно.

      Во первых такой класс, даже если он чисто управление передаёт - очень быстро растёт. Во вторых, он так-же быстро обрастает доп условиями - когда требуется контекст. Например пришел nullpointer - надо ресурсы освободит, значит надо как-то ошибку вернуть в точку вызова, или когда нужно добавить дополнительные данные для аналитики, но варианты этих данных разные. Или когда нужно логировать, но не все. Или когда разное количество retry надо сделать.

      Вы вероятно скажете, что надо заводить отдельные ошибки под каждые данные - но это придет после, когда у вас уже заложена логика с развилками в зависимости от контекста, и даже если нет - приведёт к еще большому разрастанию этого большого единого обработчика. И не дай бог у вас будет цепочка наследования ошибок типо MyError : Retryable : NetworkError ... - потом когда поведение одного из обработчиков надо будет специализировать - все вообще развалится.

      А ещё это усложняет вход в проект - намного менее очевидно - откуда пришла или куда пропала ошибка. Да - в теории в стак-трейсе все должно быть, но на практике были проблемы.

      Короче на долгоживущих проектах я бы не рекомендовал такой подход.

      Лучше либо локальные перехватчики для небольшого контекста (типо экрана) - там ошибку поймал - состояние экрана в нужное привёл, ретраи в нужном порядке и тайминге сделал и молодец. А если нужно, через композицию подключил какой-то из общих обработчиков, и параметризовал.

      Либо result использовать там, где ошибка внутри нашей бл. Он не так красив но намного более очевиден.


      1. ris58h
        02.08.2025 16:39

        Например пришел nullpointer - надо ресурсы освободит, значит надо как-то ошибку вернуть в точку вызова

        Под точкой вызова вы что понимаете в этом примере? Какие ресурсы освобождаем? Если пришел nullpointer - вы логируете nullpointer, а в http ответ пишите код 500.


        1. Spyman
          02.08.2025 16:39

          Ну предположим ситуация такая - мы открыли ресурс (например соединение с базой данных), а потом сделали запрос на сервер, пришел null в критически важном параметре (может он там по БЛ может быть null, может просто ошибка, не важно) - в случае чистого try - catch - ты заворачиваешь закрытие содениня с базой в finaly и знаешь - что не важно - ошибка там или успешное выполенние - соединение с базой не будет подвешено. А тут - случился nullpointer - но exception перехватился в едином обработчике исключений, а он вне контекста, который открыл соединение с базой (в нём нет ссылки на класс, который открыл содединение с базой). Это значит либо обработчик ошибок должен знать про класс, который инициировал запрос - а тогда он обо всём на свете будет знать - каша и связность, либо все должны ему свои обратные вызовы передавать для дополнительных действий при получении ошибки - а тогда это ничем не лучше, чем try catch по месту вызова запроса. И чтобы это работало - калбек должен прицепляться именно к запросу т.к. на разные запросы, даже в рамках одного контекста, могжет быть разная обработка одних и тех-же exception.


          1. QweLoremIpsum
            02.08.2025 16:39

            в C# (в других я языках должно быт похожее) блок using гарантирует освобождение ресурсов даже при возникновении исключения внутри блока.


          1. QweLoremIpsum
            02.08.2025 16:39

            В общем случае общий обработчик ошибок не отменяет try catch finally. Не нужно туда логику финализации перемещать. Есть места где нужно try finally а есть где не нужн. В вашем примере вам нужно обернуть обращение к бд в try / finally , в finally освободить ресурсы, далее вы все равно не сможете идти по логике программы т.к. у вас нет данных, поэтому вам нужно заново выбросить то же самое исключение, чтобы оно в итоге обработалось в едином обработчике ошибок


            1. Spyman
              02.08.2025 16:39

              Логика может быть сложнее, чем освободить ресурсы - например в зависимости от ошибки - нужно освободить не все ресурсы, а только определённую часть. И тогда в finlay придется получать исходное исключение - и вот уже обработка ошибки размазана на две части а очевидность потеряна.

              Или тоже самое - нужно перестроить ui c учетом ошибки. Для этого viewModel должна вернуть в View состояние ошибки, но оно уже было перехвачено на уровне единого обработчика - а значит либо он должен во viewModel передать данный (читай иметь ссылку), либо в finlay проверяем наличие exception на уровне viewModel - но у нас опять - половина ошибки в едином контроллере, а половина в viewModel слое. А если нам еще параметры передать из viewModel в логгер для ошибки надо например - вообще пиши пропало.


          1. fori
            02.08.2025 16:39

            У нас на dotnet try-finally обработчиков много локальных, в том числе синтаксический сахар с using, а try-catch чаще всего один глобальный где и происходит логирование и формирование кода ошибки. Но есть кое где retry и другие специфические места, где нужна локальная обработка, отличная от try finally.


            1. Spyman
              02.08.2025 16:39

              Ну я так понял из коммента, что у вас скорее локальные перехватчики - вида try finaly - где finaly в том числе проверяет и обрабатывает exception. Это не про глобальную перехватку как раз, а в целом хороший подход.

              Единый обработчик ошибок который я имел ввиду, это когда в условном singletone лежит switch(error) case на сотню вариантов, где на каждый вариант вызывается некоторый обработчик конкретной ошибки.


              1. withkittens
                02.08.2025 16:39

                try-finally не ловит и не обрабатывает исключения (в нём нет блока catch).


          1. ris58h
            02.08.2025 16:39

            Как вам уже успели ответить, никто вам не запрещает обрабатывать исключения не глобально. Это база работы с исключениями: можешь обработать - обработай, не можешь - не обрабатывай.


  1. dmitriy_minaev
    02.08.2025 16:39

    goto не любят, потому что от него можно прыгнуть куда угодно по логике. И ситуация сильно ухудшается, если эти goto расставлены в нескольких местах и становятся перекрестными. Сравнивать throw с goto некорректно, так как throw явно ведёт либо вниз по коду, либо на какой-то уровень выше, то есть throw в этом смысле более линеен и предсказуем. Собственно, поэтому он и сделан: если есть большая вложенность и надо резко выпрыгнуть, минуя какую-то логику, которую, допустим, писали не вы. Да, его не нужно использовать часто, это обработчик специфических ситуаций и я тоже предпочитаю чаще использовать return, но это не всегда оправдано.

    Вообще, я напомню, throw вводился для отделения логики работы от логики обработки ошибок. Т.е. в теории с ним более чистый код.


    1. ryo_oh_ki
      02.08.2025 16:39

      Дейкстра критиковал "goto" за то, что этот оператор позволяет создавать произвольные переходы в коде, включая переходы между функциями (т.е. игнорируя стандартный вход по имени и выход по return) или внутри программы, что нарушает структурированность и предсказуемость программы. И не более того. А дальше уже мифы и "не читал, но осуждаю"...

      Ирония в том, что в C/C++ аналогом "goto" Дейкстры является не сам "goto", а "longjmp", аналогично которому реализованы исключения...


      1. dmitriy_minaev
        02.08.2025 16:39

        Да, все верно, когда я писал комментарий выше - я учитывал, что goto сейчас во многих языках можно ставить достаточно ограниченно. Но это по-прежнему путает логику, особенно в комбинации с if. И я ссылался не на Дейкстру, а на собственный опыт писания, например, на ассемблере. Достаточно быстро понимаешь, зачем логика делится на блоки и почему почти всегда goto можно и нужно менять на вызов функции, в которую выносится участок кода.


        1. SpiderEkb
          02.08.2025 16:39

          goto и вызов функции принципиально разные вещи.

          Если представить ход выполнения как некую "тропинку", то вызов функции - уход с этой тропинки в сторону с обязательным возвратом в ту же точку откуда ушли.

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


  1. panzerfaust
    02.08.2025 16:39

    Нет, декларативная обработка ошибок через монады не серебряная пуля. На практике код с Result/Either/Optional в сложной логике тоже превращается в кашу.


    1. IUIUIUIUIUIUIUI
      02.08.2025 16:39

      А можно пример такой практики? Потому что в моей практике с Either/MonadError выходит достаточно читаемо.


      1. mvv-rus
        02.08.2025 16:39

        Кем читаемо? Прочтет ли это простой вайб-кодер? :-)


        1. IUIUIUIUIUIUIUI
          02.08.2025 16:39

          Но ведь вайб-кодеры не читают код!


  1. NeoNN
    02.08.2025 16:39

    Result success/fail, и его дальнейшее развитие в виде functional extensions хороши в том случае, когда мы делаем цепочку логики, из которой можем вывалиться с ошибкой на любом шаге, так называемое railway-oriented программирование. Про него в C# очень хорошо Владимир Хориков рассказывал. А исключения на то и исключения, что ими не следует делать второй контур логики, вводя кучу пользовательских и обрабатывая их то тут, то там, что приводит к разрушению связности кода и появлению неявных зависимостей. В основном такая претензия к исключениям, когда их не по назначению используют.


    1. MountainGoat
      02.08.2025 16:39

      Исключения используют крайне избыточно. Лично у меня есть правило, которое в грубом виде звучит так: обработчик исключения всегда должен заканчиваться на sys:exit(). Если событие не повод останавливать процесс или хотя бы поток, то это и не повод использовать исключение.

      А некоторые популярные библиотеки наоборот считают исключения решением всего. Функция имеет 3 режима работы? Добавим ей аргумент int mode и исключение, если mode > 2. С точки зрения архитектуры, это орки писали.

      Невалидные состояния системы должны не иметь валидного представления в коде. Это классическая школа проектирования, и в ней не нужны исключения. Если открываешь файл, а он не открывается, то поступать можно так: ввести два типа: ОткрытыйФайл и НеоткрывшийсяФайл. И функция открытия файла возвращает одно из двух. Потом результат надо различить по типу. И все функции работы с файлом принимают только ОткрытыйФайл. Всё. Ошибится - невозможно, компилятор не даст. Поток выполнения не нарушается. Компилятору легко оптимизировать.


      1. VADemon
        02.08.2025 16:39

        Функция имеет 3 режима работы? Добавим ей аргумент int mode и исключение, если mode > 2.

        А чем это отличается от assert?

        Или, судя по остальной части комментария, надо чтобы возможности неограниченной int переменной не было вовсе, а только Enum, для которого каждое состояние строго определено?


        1. MountainGoat
          02.08.2025 16:39

          А чем это отличается от assert?

          Ничем. assert-ы это хорошо. Особенно когда их много, а в релизной сборке их можно отключить и это не инвалидирует всё тестирование, проведённое в дебаге.

          Преднамеренно необработанные исключения это тоже бывает норм, там sys:exit() сам вызывается.

           надо чтобы возможности неограниченной int переменной не было вовсе

          Именно так. И главное - с результатом вызова то же самое. Из функции не должно вылетать то, что не написано в её заголовке. Например, исключения.


      1. mahairod
        02.08.2025 16:39

        Добавим ей аргумент int mode

        Используйте енум, и будет вам счастье, (если это не случай си-подобного языка типа джс)

        С точки зрения архитектуры, это орки писали.

        Вы- наверное OpenGL никогда не пытались вызывать

        два типа: ОткрытыйФайл и НеоткрывшийсяФайл

        Нил, как неоткрытый файл тоже признается в современном мире злом.


      1. Ndochp
        02.08.2025 16:39

        Аргумент больше двух когда есть три режима работы это синтаксическая ошибка, которую не смог поймать компилятор. Если уж не смогли написать так, чтобы поймал, то остается только падать мне кажется.
        Если могли бы, но не стали морочиться - то конечно зря.


        1. MountainGoat
          02.08.2025 16:39

          А в реальности обычно всё не так очевидно. введут три-четыре параметра, которые взаимно ограничивают друг-друга, запрещая определённые комбинации. А у тебя часть этих параметров идёт напрямую из пользовательского выбора. Короче, пока не вызовешь функцию и получишь исключение - фиг ты сможешь узнать, что хотелки пользователя невозможны и ему нужно сказать выбрать другое.

          Не люблю компоненты, у авторов которых Си головного мозга.


          1. Ndochp
            02.08.2025 16:39

            Так делать то что? вот в тебя влетела невозможная комбинация. Ну и пошли исключение вызывающему. Хочет жить без исключений - пусть обкладывает вызов условием на совместимость входных параметров и заранее говорит пользователю, что он не то навыбирал. Не хочет - пусть ловит исключения. Все равно дальнейшая жизнь невозможна.


      1. ProgerMan Автор
        02.08.2025 16:39

        Я о том и говорю, что исключения должны быть только там, где программа даже не должна работать дальше. Если в API вместо положительного числа пришло отрицательное, то это просто ошибка валидации и мы возвращаем BadResult(ModalState). Если же кидать исключение на каждую проверку вместо того, чтобы вернуть сообщение об ошибке и далее тот же BadResult с этим текстом, то ничего хорошего из этого не выйдет.


      1. inv2004
        02.08.2025 16:39

        И функция открытия файла возвращает одно из двух

        Это невозможно в языках с нормальной типизацией


  1. php7
    02.08.2025 16:39

    А if без {} в вашем языке не дурной тон?


    1. voidinvader
      02.08.2025 16:39

      Общего консенсуса нет, да и многие фичи добавляли в C#, по ощущениям, чтобы угодить сразу всем и через синтаксический (и не только) сахар переманить на язык как можно больше разработчиков.


    1. ProgerMan Автор
      02.08.2025 16:39

      Нет, просто мне лично нравится, когда в один экран влазит больше кода, а пустые строки только там, где я их оставил.


      1. DieSlogan
        02.08.2025 16:39

        О, помню один кейс, в файле было неправильное выравнивание и строка следующая за if сдержала throw И была где-то на востоке экрана. А в той строке, что была на экране под if был норм запись в лог. И человек спрашивал, откуда exception?


  1. php7
    02.08.2025 16:39

    В вашем языке goto может прыгнуть куда угодно?

    В моем возможны только в том же контексте. То есть внутрь функции или цикла не прыгнешь.


    1. vadimr
      02.08.2025 16:39

      Для этого придумали call/cc :)


    1. mahairod
      02.08.2025 16:39

      Даешь setjump в массы!


  1. impwx
    02.08.2025 16:39

    Без поддержки со стороны языка и фреймворка подход с Result превращается в гору бойлерплейта и не дает никаких гарантий, т.к. методы BCL все равно кидают исключения. А еще может быть ситуация, когда у вас много реализаций Result, например из двух разных NuGet-библиотек и еще ваша собственная, и они не совместимы между собой...


  1. mahairod
    02.08.2025 16:39

    Так пиши(те) на Си. И делайте везде проверки вручную, если нравится. И компилируй(те) с флагами -noexceptions -nortti. Только без гоуту.

    А ещё лучше на ассемблере (только чур без goto):)).

    Конечно, тогда ещё, возможно, не знали о возможности возврата ошибки вместо её выбрасывания

    Я вам, возможно, открою глаза, но исключения придумали ровно для того, чтобы не заморачиваться постояннными проверками в Си-стиле. И это было ПОСЛЕ си-подхода.


    1. winorun
      02.08.2025 16:39

      Я Вам, возможно, открою глаза, но исключения придумали ровно для того, что бы обрабатывать исключения


      1. MonkeyWatchingYou
        02.08.2025 16:39

        Поддержу, так как обработка исключений не равно обработке ошибок.


    1. Koyanisqatsi
      02.08.2025 16:39

      Разве эти флаги не для C++ компилятора? В чистом Си такого нет.


      1. mahairod
        02.08.2025 16:39

        Конечно, плюсы, вернее ГЦЦ и точнее так: -fno-rtti -fno-exceptions


  1. VADemon
    02.08.2025 16:39

    Но почему-то я не встречал никакого негатива насчёт throw. А ведь это точно такая же фигня, если даже не хуже.

    Ну почему же? Я тут на днях почитывал дискуссии давно минувших лет, тогда были еще совершенно другие идеи по поводу того, куда и как должно развиваться структурное структурированное программирование.

    А раз появились исключения, значит какую-то проблему ими пытались в свое время решить? Как "старый метод" решения проблемы в языках, где можно вернуть только одно значение (без оборачивания в struct/object): те самые C API. Где, если вместо handle или чего-то там вернулось null/-1, то посмотри на GetLastError() и т.п. И оказалось, что статическая errno на всю программу - не очень-то thread safe и так далее.


    1. apevzner
      02.08.2025 16:39

      И оказалось, что статическая errno на всю программу - не очень-то thread safe и так далее.

      И быстренько решили эту проблему, сделав errno thread-local...


      1. VADemon
        02.08.2025 16:39

        Не назвал бы быстреньким. В документацию по cpp посмотрел, говорится: многопоточные библиотеки следующие стандартам C11, C++11 должны сделать их thread-local. А это (вики) конец 2011 года. Неосведомлен, правда, что там было вне стандарта.


        1. apevzner
          02.08.2025 16:39

          А в POSIX это когда появилось?


          1. VADemon
            02.08.2025 16:39

            "Programs should obtain the definition of errno by the inclusion of <errno.h>. The practice of defining errno in a program as extern int errno is obsolescent. "

            System Interfaces and Headers, Issue 5: Volume 1. 1997

            стр. 189 pdf

            issue 6. см. изменения

            https://pubs.opengroup.org/onlinepubs/009696799/functions/errno.html

            Если я правильно понял (опережу: да), то задепрекейтили только в пятом издании.

            issue 4, стр. 130, 1994

            conformant systems may support the declaration:
            extern int errno;

            https://pubs.opengroup.org/onlinepubs/9695969499/toc.pdf

            Еще высказывания Торвальдса по теме самого errno: http://yarchive.net/comp/linux/errno.html


            1. apevzner
              02.08.2025 16:39

              Это про то, что errno - не обязательно настоящая переменная, а может быть и макрос.

              Но в целом, сами по себе POSIX threads стандартизованы в 1995-м...

              Торвальдс очень категоричен.


              1. VADemon
                02.08.2025 16:39

                Из того, что errno стало макросом, следует, что его можно переопределить и сделать thread-local. Как иначе?


                1. apevzner
                  02.08.2025 16:39

                  extern __thread int errno;

                  Ну или по-Go-шечному, сделать errno таким волшебным словом, которое обозначает не просто переменнию, а тред-локальную. В конце концов, memcpy примерно так и устроен в ряде компиляторов.


                  1. VADemon
                    02.08.2025 16:39

                    Видимо, решили не наступать дважды на те же грабли однозначного значения символа в стандарте?

                    Торвальдс категоричен, не отнять. Подумалось, может это такой вариант rage-bait'а двадцатилетней давности? Понятно, что это мягко говоря... кхм, "особенности" характера, но может она его и возвела в культовый статус?

                    С наводкой именно на pthreads теперь и докопался окончательно:

                    https://unix.org/version2/whatsnew/threadspaper.pdf

                    10.13 Redefinition of errno

                    .

                    In POSIX.1, errno is defined as an external global variable. But this definition is unacceptable in a multi-threaded environment, because its use can result in non-deterministic results. The problem is that two or more threads can encounter errors, all causing the same errno to be set. Under these circumstances, a thread might end up checking errno after it has already been updated by another thread.

                    .

                    To circumvent the resulting non-determinism, POSIX.1c redefines errno as a service that can access the per-thread error number as follows (ISO/IEC 9945-1: 1996 (POSIX-1), §2.4):

                    .

                    ‘‘Some functions may provide the error number in a variable accessed through the symbol errno. The symbol errno is defined by including the header , as specified by the ISO C standard ... For each thread of a process, the value of errno shall not be affected by function calls or assignments to errno by other threads.’’

                    .

                    In addition, all POSIX.1c functions avoid using errno and, instead, return the error number directly as the function return value, with a return value of zero indicating that no error was detected. This strategy is, in fact, being followed on a POSIX-wide basis for all new functions.

                    Отдельно хочу обратить внимание на последний параграф. Я бы сказал, обычный просчет архитектуры API. Об ошибках этого рода как-то не принято вспоминать и говорить? Хотя очень помогло бы в виде пост-мортемов для разбора "полетов".


                    1. apevzner
                      02.08.2025 16:39

                      Отдельно хочу обратить внимание на последний параграф.

                      Возврат errno по значению вносит сумятицу, когда невозможно выделить какое-то специальное невалидное значение

                      Особенно обидно, когда для всех функций API он подходит, а для парочки каких-то надо изобретать отдельный механизм

                      Ну и конечно, то, что в UNIX все функции при ошибке возвращают -1, а pthread_XXX - 0, не добавляет удобства.

                      В этом плане, конечно, Go-ный подход, возвращающий ошибку отдельным значением (или подход языков с алгебраическими типами, возвращающими результат или ошибку через maybe-тип) выглядит более удачным. Но если API на Си, тут, наверное, совсем уж хорошо не сделаешь...


                      1. VADemon
                        02.08.2025 16:39

                        Согласен.


    1. ProgerMan Автор
      02.08.2025 16:39

      Они и продолжают решать определённые проблемы, например, когда в DI не дописали интерфейс и у нас случился NullReferenceException в методе или ArgumentException в контроллере, если туда это дописали. Это ошибка программиста, которая решается один раз и там больше не должно быть исключения. А пользователь, соответственно, не должен видеть содержимого этого исключения.

      Но если нам просто надо провалидировать данные и сообщить пользователю о неверных данных, это подобно использованию топора там, где нужен пинцет.

      Я считаю, что исключение - это такое сообщение, текст которого конечный пользователь не должен видеть. Он только для внутреннего использования и в идеале исключение не должно повторяться, если его один раз поправили.


  1. mahairod
    02.08.2025 16:39

    Поскольку статья заявлена как туториал, а не мнение, ставлю минус - слишком самоуверенный подход к поучению спецов у вас.


    1. mvv-rus
      02.08.2025 16:39

      Поступил точно так же. Комментарий по статье тоже оставил.


  1. boulder
    02.08.2025 16:39

    А где же вариант: "Ничего из вышеперечисленного"? )


  1. AlexCzech01
    02.08.2025 16:39

    Всё, что угодно, надо использовать только там, где оно действительно необходимо, а не везде где попало. Вы так можете написать целый цикл статей или даже книгу
    - Используйте циклы только там, где они действительно необходимы
    - Используйте протокол HTTP только там, где он действительно необходим
    - Используйте микросервисы только там, где они действительно необходимы
    и так далее

    Проблема обычно не в том, чтобы такое общее утверждение выдать, а в том, чтобы понимать, когда оно действительно необходимо, а когда нет

    PS. Ловить неспецифичный exception в контроллерах - тупейшее занятие. Для этого придумали миддлвары. В этом проблема кода "было", а не в том, что в программе внутри exception выбрасывались


  1. feruxmax
    02.08.2025 16:39

    Пока нет Result-а из коробки предпочитаю создавать явно типы как:

    internal abstract record CreateResult
    {
        public sealed record Success(SomeModel Model) : CreateResult;
        public sealed record NotFound : CreateResult;
        public sealed record WrongOperation : CreateResult;
    }
    

    который используется в controller-е как:

    CreateResult result = await _service.Create(...);
    
    return result switch
    {
        CreateResult.Success data => TypedResults.Ok(data.Model),
        CreateResult.NotFound => TypedResults.NotFound(),
        CreateResult.WrongOperation => TypedResults.BadRequest(),
        _ => throw new ArgumentOutOfRangeException(result.GetType().Name, "Unexpected result type")
    };
    

    Если объявление CreateResult и использование в разных сборках, то дефолтный branch в switch нужен пока, к сожалению, но это отдельный вопрос


    1. granit1986
      02.08.2025 16:39

      del


  1. ncix
    02.08.2025 16:39

    goto? Все его ненавидят,

    Серьезно? А что кто-то с ним реально сталкивается сейчас, кроме программистов на ассемблере? Начало статьи выглядит так как будто написана 30 лет назад, когда goto в реальном коде ещё встречалось.

    А Exception'ы эффективны именно при большой вложенности кода, особенно когда часть слоёв вообще не ваши.


    1. withkittens
      02.08.2025 16:39

      Серьезно? А что кто-то с ним реально сталкивается сейчас, кроме программистов на ассемблере?

      Разумеется. В рантайме того же нашего дотнета уйма goto.


      1. Vitimbo
        02.08.2025 16:39

        Полагаю, поинт был не про библиотеки и их внутренности, а про код, который ты или твоя команда пишет. Лично я даже не знаю, какой синтаксис у goto и пользовался им еще на Си в универе.


        1. withkittens
          02.08.2025 16:39

          Полагаю, поинт был не про библиотеки и их внутренности, а про код, который ты или твоя команда пишет.

          А в чём принципиальная разница?


          1. Vitimbo
            02.08.2025 16:39

            Так можно дойти до того, что мы, в конечном итоге, пишем процессорные команды, а там ни goto ни throw не водится и весь спор теряет смысл.

            В статье же речь про повседневную жизнь и то, что люди пишут и читают каждый день.


            1. withkittens
              02.08.2025 16:39

              Ну вот потребность выхода из вложенных циклов - повседневная жизнь.


    1. ProgerMan Автор
      02.08.2025 16:39

      Они эффективны, пока ты не начинаешь читать такой код. Разные исключения обрабатываются по-разному и никогда не угадаешь, как очередное исключение будет обработано, пока не прочитаешь код всего стека вызовов методов от контроллера до твоего десятого по счёту метода.

      goto тоже по-своему эффективен, пока не начинаешь читать такой код. (Вспомнилось, как последний раз я их использовал как блок, подобный finally. Это было лет 15 назад в Delphi.)


      1. VADemon
        02.08.2025 16:39

        В ядре Linux они до сих пор как finally и используются: освободить ресурсы и на выход.


      1. ncix
        02.08.2025 16:39

        Разные исключения обрабатываются по-разному и никогда не угадаешь, как очередное исключение будет обработано

        А может их просто не для того используют в конкретном проекте? Когда появляются множества обработчиков множества разных видов исключений - это тревожный звоночек, возможно что-то не так с архитектурой. Когда приходится залазить во внутренние слои чтобы понять как работают внешние - это уже громкий звонок. Что-то не так с принципами SOLID.

        Вспомнилось, как последний раз я их использовал как блок, подобный finally. Это было лет 15 назад в Delphi.

        В Delphi всегда был нормальный finally


    1. NeoNN
      02.08.2025 16:39

      Когда пишешь async await - внутри живёт код стейт машины с goto :)


      1. ncix
        02.08.2025 16:39

        goto где-то внутри почти любого кода, но напрямую мы его не используем очень давно.


  1. KReal
    02.08.2025 16:39

    Присмотритесь к OneOf


  1. fork1488
    02.08.2025 16:39

    давайте уже забудем о питоне


  1. PiterP
    02.08.2025 16:39

    Мне кажется человек с такими утверждениями не может считаться хорошим программистом. Мало того, что утверждение про goto ложно и вытекает из некоторых нежелательных моментов при его использовании, но автор этого высказывания подчеркивал, что именно избыточное и необдуманное использование goto может привести к проблемам. Автору посоветую глянуть код BSD или Linux, для просвещения. Тем более после компиляции C кода Goto преобразуется в JMP, что более эффективно чем всë остальное, но вы про эффективность не слышали?

    Теперь пройдемся про throw, оно придумано и рекомендовано к использованию в любых ситуациях, что прямо прописано в документации Java, и там же разобрано как с ним работать, думаю вы не осилили это, раз появилась очередная холиварная статья. Так же в документации и курсах от Microsoft так же четкая рекомендация использовать throw. Если они для вас не авторитеты, то даже не знаю как характеризовать эту статью не прибегая к грубым эпитетам.

    Почему не любили throw раньше? На слабых компьютерах и ограниченном объеме в реализации этого на C++ получался огромный оверхед, который съедал относительно много памяти и процессорного времени. Отсюда пошли статейки, что лучше не использовать throw, а только в критических местах. В java и прочих интерпретаторах или JIT языках такой проблемы нет, а сейчас и с памятью и мощностью процессоров проблем нет, так что не надо выдумывать проблемы где их нет, а просто научитесь писать правильный код.


    1. VADemon
      02.08.2025 16:39

      В java и прочих интерпретаторах или JIT

      Статья по теме Hotspot JVM: https://shipilev.net/blog/2014/exceptional-performance/


      1. PiterP
        02.08.2025 16:39

        Спасибо, очень интересная статья, но она о сферическом коне в вакууме и приводить еë как аргумент как минимум не уместно, т. к. в итоге мы видим, что на самом деле эффективность в пределах разумного, а писать критические приложения где сборщик мусора может в любую секунду обнулить все оптимизации, такое себе) Опять же хочу обратить внимание, что данный оппус был создан пользователем Хабр, что не является истиной в последней инстанции, а в свете погони авторов за рейтингами вообще критически отношусь ко всем публикациям на Хабр, к чему и призываю всех людей. А то потом эти необоснованные статьи попадают в индексацию и тем самым влияют на неокрепшие мозги других пользователей и они начинают следовать призывам и утверждениям в данных статьях. Ещë раз, лучше придерживаться рекомендациям от производителей и разработчиков языков, а там черным по белому написано "Используйте исключения, они эффективны для большинства ваших задач"


        1. VADemon
          02.08.2025 16:39

          Да, только Алексей потом с Хабра и ушел (ЕМНИП из-за несогласия с политикой сайта) и вел сам себе блог на сайте. Но помимо этого он перформансом в OpenJDK долгое время и занимался: всё то, что под капотом.

          А ссылку мне стоило подписать, что ничего там страшного и противоречащего нету. За это извиняюсь :)


        1. feruxmax
          02.08.2025 16:39

          а там черным по белому написано "Используйте исключения, они эффективны для большинства ваших задач"

          У нас черным по белому написано так
          Do not use throwing or catching exceptions as a means of normal program flow, если уже критически относится. При этом эти наши кейсы из бизнес логики ведущие к возвратам тех же NotFound - normal program flow


          1. PiterP
            02.08.2025 16:39

            Замечательно и где противоречия с моими высказываниям? И на сколько NotFound это нормально? Давайте не будете врать ни мне ни себе. Это ошибка поведения программы, возврат значения которое не является успешным. 403 это ОШИБКА. Это уже не Normal Flow. И тут вопрос как обрабатывать лучше и удобнее. Ничто не мешает, в том числе и ваша ссылка на MS, использовать исключения и консолидированную обработку на уровнях ниже, можно даже прописать каждому исключению свой класс, что будет смотреться и обрабатываться удобнее. Но конечно можно промудрить по классике с Result. Сколько людей, столько и мнений?

            Тут ситуация такая вырисовывается:

            Представим: Брюки это Исключения, Килт это Result

            Автор: "Если пойти в туалет и не снять Брюки, то может произойти казус, а вот если использовать Килт, то это минимизирует ущерб"

            Комменты:" Да точно, вы правы Брюки надо расстегивать и да Килт это вроде круто! "

            Автор: " Если расстегнуть ширинку на Брюках, но при этом стоять против ветра, то может произойти казус, а вот при использовании Килта такие проблемы минимизированы. "

            Комменты: "О да, делать это против ветра это конечно не разумно, Килт выглядит очень удобно! "

            Автор: " ну вот используйте Килт чаще чем Брюки! "

            Комменты:"Стопэ, Килт конечно это круто, но мы привыкли использовать Брюки, и да мы не забываем их расстегивать и стараемся контролировать направление ветра, поэтому извини дружище, но мы уже в Брюках походим. "


            1. feruxmax
              02.08.2025 16:39

              Замечательно и где противоречия с моими высказываниям?

              Как минимум считаю полезным читающим видеть как сформулировано официально у ms, в противовес вашему утверждению что рекомендуют «Используйте исключения, они эффективны для большинства ваших задач», а дальше каждый для себя решит, что ему ближе.

              Это уже не Normal Flow

              Да, тут возможно не на кого сослаться для определения, хотя в тех же проектах-примерах ms, как eshop, NotFound через исключения не кидают. Ну и в целом если представить, что у нас добавление товара в корзину возвращает NotFound когда товар закончился (что случается давольно часто), чем это не normal program flow


    1. Gromilo
      02.08.2025 16:39

      Автору посоветую глянуть код BSD или Linux, для просвещения.

      А там разве goto не используется в одном конкретном паттерне для очистки ресурсов, чисто из-за ограничений C?

      Почему не любили throw раньше?

      У моего коллеги был легаси проект, где логику построили на исключениях. Говорит, очень не понравилось распутывать все цепочки вызовов, чтобы понять как и что работает.


      1. PiterP
        02.08.2025 16:39

        Но если почитать заголовок и хейт на goto то создаётся впечатление, что goto это зло.

        И опять же это не из-за C, а из-за процессора, Goto это jmp. Для адептов антиГото, можно было сделать вместо Goto очищающую функцию, в которую передавали бы параметры очистки и т. д. Но нет, используют не потому что ограничение C, а потому что вызов функции медленнее в разы, а тут раз и вышел если всë хорошо.

        Пример интересный, но goto и throw это инструмент, а не модная штука. Просто надо руководствоваться здравым смыслом. Проекто с ужасными result которые без слез не раскрутишь тоже не мало.


        1. Gromilo
          02.08.2025 16:39

          Но нет, используют не потому что ограничение C, а потому что вызов функции медленнее в разы, а тут раз и вышел если всë хорошо.

          Именно это и имел ввиду. Нет другого эффективного способа очистить ресурсы.

          Просто надо руководствоваться здравым смыслом. Проекто с ужасными result которые без слез не раскрутишь тоже не мало.

          Да всё плохо, везде сложно.


  1. michael_v89
    02.08.2025 16:39

    Исключения должны использоваться для исключительных ситуаций, а не для возврата значения, тогда с ними проблем не будет. Это означает, что их не нужно нигде ловить, кроме глобального обработчика. Когда конкретный тип исключения ловится неизвестно где выше по стеку, это и есть goto. Их можно ловить, чтобы откатить действие (например транзакцию в базе данных) - поймали, откатили, бросили дальше. Или когда это второстепенное действие, и ошибка не должна останавливать основное, например, отправка уведомлений после создания заказа.


    1. PiterP
      02.08.2025 16:39

      Расскажите нам, что плохого в Goto, что бы дальнейшие ваши претензии были обоснованы. Просто такое ощущение, что у большинства людей поливающих грязью Goto нет понимания для чего и где он используется.

      Во вторых если вы почитаете про исключения, наконец от разработчиков, то обнаружите, что поведение исключений не сильно отличается от простых return, просто заданных неявно. И если вы используете return, то вы попадаете на то, что вы вынуждены разворачивать всегда всю цепочку вызовов, даже если на верху что-то пошло не так и выполнять данный набор команд(функций и т. д.) не имеет смысла и нужно перейти к другим. Именно это делают исключения, убирая ненужные return для вложенности, а сразу переходя к коду обработчку(ловушке).

      И да, просто возвращать положительный результат это не очень хорошая практика, но это нигде не запрещено явно и руководствуется только здравым смыслом, как и с оператором Goto, всë остальное это ваше ИМХО.


      1. michael_v89
        02.08.2025 16:39

        Расскажите нам, что плохого в Goto

        Я же написал: "ловится неизвестно где". Это означает, что из вызываемого кода сложно найти место, куда возвращается управление, а в вызывающем сложно понять, что вообще может прилететь и что надо ловить. Особенно если там "иерархия исключений", и бросается одно исключение, а ловится его произвольный родительский класс.

        поведение исключений не сильно отличается от простых return, просто заданных неявно

        Да, я так и написал, "неявно" в данном случае это синоним "неизвестно где".

        Во вторых если вы почитаете про исключения, наконец от разработчиков

        Я сам разработчик, и у меня есть свое мнение об этом.

        И если вы используете return, то вы попадаете на то, что вы вынуждены разворачивать всегда всю цепочку вызовов

        Если вы не поняли, я не предлагаю использовать подход, который описан в статье. Я написал, как надо работать с исключениями, чтобы они не создвали проблем, про которые пишет автор.

        Именно это делают исключения, убирая ненужные return для вложенности, а сразу переходя к коду обработчку

        Я в курсе.

        всë остальное это ваше ИМХО.

        Любые правила для кода это ИМХО разработчиков. Вы можете писать код в одну строку с однобуквенными переменными, только ИМХО других разработчиков будет в том, что это хреновый код, который сложно поддерживать.


        1. PiterP
          02.08.2025 16:39

          Мои придирки были обращены к оператору goto, который является всего лишь инструментом, но который необоснованно хейтят и приплетают ситуацию с ним в прошлом к другим проблемам, вызывая негатив и отторжение к данному инструменту. Изначально кампания была по ограничению и правильному использованию GOTO, которая переросла в довольно кривобокую картину, когда молодые разработчики уверены, что GOTO это плохо и боятся применять его или даже не знают о его возможностях. Это не то к чему надо стремиться и статья вызывает критику в том числе и кликбейтным заголовком и необоснованным принижением инструмента разработки.

          Я с вами полностью согласен, что всë программирование это ИМХО, но при этом вы высказывкетесь в довольно категоричной форме:"Я написал как надо...", это читается, что всë остальное не надо?) Понимаю что это может быть оборот речи у вас, но всë же... Вы и выше утверждаете, что только глобальный обработчик это правильно. Но с вами несогласны именно разработчики языков, о которых я говорил, или вы участвуете в разработках языков Java и т. д. ? Думаю я непонятно выразился... Увлекаться глубиной fast-return согласен не всегда хорошо, но и прям пренебрегать как вы данным инструментом, как вы советуете тоже сомнительно.


          1. michael_v89
            02.08.2025 16:39

            но который необоснованно хейтят

            Хейтят его вполне обоснованно. Личный опыт разработчика это достаточное обоснование. А большое количество таких разработчиков уменьшает вероятность, что кто-то что-то не так понял.

            когда молодые разработчики уверены, что GOTO это плохо и боятся применять его

            Правильно уверены, и применять его не надо, если есть возможность сделать по-другому.

            или даже не знают о его возможностях

            Возможность там одна, переход в произвольное место программы.

            но при этом вы высказывкетесь в довольно категоричной форме:"Я написал как надо..."

            В первом комментарии я это не писал. Во втором понятно по контексту, что это мое мнение. Я там так и написал "у меня есть свое мнение".

            Я говорю "применять его не надо", потому что точно знаю, что мне будет сложно работать с таким кодом, и могу представить, что другим разработчикам тоже будет сложно. Мне неважно, что лично вы считаете этот код простым и понятным. В проектах, которые будете поддерживать только вы, конечно можете делать как хотите.

            Но с вами несогласны именно разработчики языков, о которых я говорил

            Насколько я могу судить, разработчики языков, о которых вы говорили, со мной согласны.

            но и прям пренебрегать как вы данным инструментом, как вы советуете

            Я не советую пренебрегать данным инструментом. Бросать исключения можно и нужно. Ловить их, тем более по специфичному типу, и продолжать выполнение в большинстве случаев не надо. Исключительная ситуация должна останавливать программу, потому она и исключительная.


            1. PiterP
              02.08.2025 16:39

              "Правильно уверены, и применять его не надо, если есть возможность сделать по-другому."

              Совсем неверный постулат, если инструмент работает лучше чем другие варианты, то лучше применить этот инструмент. И хороший мастер должен знать где и когда лучше применять инструмент. Не надо молотком забивать шурупы, а отверткой закручивать гвозди. Goto тоже можно и нужно использовать, а что бы знать когда и где нужно не хейтить, а обучать, разъяснить. Вы предлагаете мыслить шаблонами, как возможно вас приучили это делать, не спорю это тоже результативно.

              "Ловить их, тем более по специфичному типу, и продолжать выполнение в большинстве случаев не надо. Исключительная ситуация должна останавливать программу, потому она и исключительная."

              В этом вся проблема шаблонов, вы почему-то твердо уверовали, в то, что только ваше применение это правильно, но в документации Java отлично показано, что можно использовать в том числе и fast-return. И хочу вас просветить, Исключения придумали не для того что бы останавливать программу, а именно для того, что бы по возможности не останавливать еë.


              1. michael_v89
                02.08.2025 16:39

                Совсем неверный постулат, если инструмент работает лучше чем другие варианты

                Ну вот дело в том, что в большинстве случаев он не работает лучше, чем другие варианты. Дальше надо приводить конкретные примеры, а не общие слова.

                Вы предлагаете мыслить шаблонами, как возможно вас приучили это делать

                Еще раз объясняю, мое мнение о goto основано на личном опыте, а не на чьих-то шаблонах.

                вы почему-то твердо уверовали

                Еще раз объясняю, я не "уверовал", а "проверил". Почему, я уже написал - потому что я точно знаю, что мне будет сложно работать с таким кодом.

                Исключения придумали не для того что бы останавливать программу

                Предлагаю вам посмотреть в словаре, что означает "исключение" в русском языке, "exception" в английском, и подумать, почему этот механизм так назвали. Если программа получила исключение, что база данных недоступна по сети, то она должна остановиться, а не продолжать алгоритм с пустыми данными.


                1. PiterP
                  02.08.2025 16:39

                  Я тоже пишу на личном опыте. Спасибо.

                  "Если программа получила исключение, что база данных недоступна по сети, то она должна остановиться, а не продолжать алгоритм с пустыми данными."

                  Пример хорош, но с таким же успехом я могу привести пример ожидания появления файла в файловой системе, вы каждый раз будете дергать глобальный обработчик или всë же будете действовать по ситуации и ловить исключения там где это логично, в функции проверки наличия файла файловой системы, не доводя до глобального обработчика? Как раз тут ситуация как не остановить программу, а заставить еë работать и ждать результата.


                  1. michael_v89
                    02.08.2025 16:39

                    Вообще-то функция проверки наличия файла файловой системы должна возвращать boolean, а не бросать исключение. Поэтому "логично" сделать if, а не try/catch. Это как раз то, о чем говорит автор, только он слишком преувеличивает.

                    Аналогично, функция валидации входных данных должна возвращать boolean и список ошибок, а не бросать ValidationException. Обработка некорректного ввода должна быть предусмотрена, поэтому это не исключительная ситуация. А вот контроллер уже может бросить ValidationFailedHttpException, если это требуется фреймворком, а может и не бросать, а использовать метод контроллера return this.sendValidationErrorResponse(validationResult).


  1. vkni
    02.08.2025 16:39

    Ксавье Леруа (Xavier Leroy), который учёный, а не учитель танцев, недавно выпустил набор лекций по связи goto/исключений и прочих управляющих структур - https://xavierleroy.org/CdF/2023-2024/

    Очень рекомендую.


  1. jdev
    02.08.2025 16:39

    Я долго пытался есть этот кактус на Kotlin + Spring потому как теоретически мне идея нравится, но в итоге отказался.

    Потому что:

    1. прокидывать ошибки по стеку руками - через чур гемморойно, имхо

    2. Result плохо интегрируется с библиотеками. В частности Spring не откатит транзакцию, а корутины не закенсалят скоуп при возврате резалта. Транзакции точно, а коррутины скорее всего можно обработать напильником, но, опять же, через чур гемморойно, имхо

    3. В Котлине нет нормальной возможности в верхнеуровневом методе вернуть пару разных ошибок из вызываемых методов и в итоге ошибка очень быстро превращается в Exception (корневой тип ошибок), чья информативность стремится к 0. Хотя это может решиться благодаря Rich errors (доклад с их представлением).

    В итоге я разделил ошибки на восстановимые (которых в моих проектах очень мало) и не восстановимые. Восстановимые возвращаю резалтом и тут же обрабатываю, а не восстановимые бросаю исключениями которые улетают до контроллера (спецефичные для метода) или миддлваря (универсальные в духе resource-not-found). Всё это у меня подробно описано тут


  1. CrazyElf
    02.08.2025 16:39

    Так у вас типичная "Проблема XY". После правильного рефракторинга обычно нет проблемы понять, в каких местах (на каких уровнях) вам нужно отрабатывать ошибки, а в каких это не нужно. Если рефакторинг правильно разделил код на слои и свёл его к типовым абстракциям, то вам не нужно бегать по всему коду и расставлять ловлю исключений или обработку ошибок. Ловить исключения нужно только на тех уровнях абстракции, где нужно их как-то обработать: записать ошибку в лог, прервать работу программы или наоборот повторить попытку, и т.д.

    А вот объект с ошибкой вы вынуждены будете отрабатывать по всей цепочке вызовов, везде проверяя, не случилась ли ошибка, или можно что-то делать дальше, это ли не кошмар?


  1. Krokochik
    02.08.2025 16:39

    Удачи вам проверить и обернуть десяток ошибок в каждом методе контроллера вместо выбрасывания ошибок


  1. taenur
    02.08.2025 16:39

    У меня перед глазами два проекта. В одном все возвраты функций это Result, во втором присутствуют исключения. Весь вопрос вокруг валидации бизнес логики, поэтому для нее заведен собственный тип исключений. Проблема result в необходимости вручную прокидывать его наверх, у нас практически нет ситуаций, где можно обработать и пойти дальше. При прокидывании, бывает что подменяют ошибку своей, обобщенной(исключения обычно не трогают). Тогда теряется контекст и цепочку придется раскручивать дальше. На проект с result повлиять не можем, на второй - еще думаем что же удобней


  1. Bonus2k
    02.08.2025 16:39

    Я не шарпист, пишу на жабе, но в жабе ты бросаешь эксепшен в любом слое, он по стеку возвращается на самый верхний слой (чаще всего это какие нибудь контроллеры), а там обрабатываешь его и возвращаешь http error с нужным кодом и описанием из эксепшена и я не представляю какой геморрой пробросить это все с помощью того же return, который фактически предназначен для возврата результата, а не ошибки. Создаётся такое впечатление, что автор в разработке недавно.


    1. feruxmax
      02.08.2025 16:39

      Это у вас в Java исключения хотя бы часть сигнатуры методов. Без этого, переиспльзование бизнес логики с исключениями в новом endpoint-е потребовало бы изучить все слои, что и где там может стрельнуть, и это все замапить в http коды + OpenAPI - то ещё удовольствие


    1. CrazyElf
      02.08.2025 16:39

      Ну так то в некоторых языках это by design. В том же Golang нет исключений и всё должно возвращать статус помимо результата.


      1. Gorthauer87
        02.08.2025 16:39

        На самом деле, они там есть, просто называются паниками


  1. Andr3y3
    02.08.2025 16:39

    И это поняли только сейчас?


  1. Nemoumbra
    02.08.2025 16:39

    Кстати, почитайте "Go statement considered harmful", там, в частности, есть разбор проблем оригинального goto.


  1. nihil-pro
    02.08.2025 16:39

    Ну ну знаю...

    Вот у нас в JavaScript, а точнее в браузерном API есть fetch – API которого выделяется на фоне других браузерных API как раз использованием result вместо throw. И как по мне, это неудобно:

    const result = await fetch('https://habr.com/');

    Во первых, нам все равно здесь нужен try/catch, потому что может произойти ошибка при отправке запроса:

    try {
      const result = await fetch('https://habr.com/');
    } catch(error) {
      // ...
    }

    А потом еще проверят result:

    try {
      const result = await fetch('https://habr.com/');
      if (result.ok) {
        // ... все хорошо
      } else {
        // все плохо
      }
    } catch(error) {
      // ...
    }

    Хотя на мой взгляд, было бы лучше так:

    try {
      const result = await fetch('https://habr.com/');
    } catch(error: RequestError | ResponseError) {
      // ...
    }

    Да и в целом try/catch лаконичнее:

    class Foo {
      data: JSON;
      
      async doWork() {
        try {
          // можно обойтись без локальных переменных в замыкании
          this.data = await HttpClient.get();
        } catch(error) {
          // ...
        }
      }
    }
    class Foo {
      data: JSON;
      
      async doWork() {
        // лишняя переменная
        const result = await HttpClient.get();
        // ложно-положительный результат
        if (result.ok) {
          this.data = result;
        } else {
          // ...
        }
      }
    }


  1. JordanCpp
    02.08.2025 16:39

    Исключения про то, что их невозможно проигнорировать как всякие там ручные result и коды ошибок. И они про исключительные ситуации, если допустим нужно открыть файл и без него работать ничего не будет, это исключение, если это просто логика программы, если нет то создать, то это простой bool.

    И тащить во весь проект result или bool api, как в SDL3. Это не преимущество, а отсутствие исключений.


    1. ProgerMan Автор
      02.08.2025 16:39

      В том проекте, где у вас сотни throw, обязательно будет что-то вроде try { ...} catch {}, в лучшем случае try { ...} catch { logger.Log(); }.


    1. IUIUIUIUIUIUIUI
      02.08.2025 16:39

      Исключения про то, что их невозможно проигнорировать как всякие там ручные result

      openFile : String → Either FileError FileHandle

      Как тут проигнорировать ручной result?


      1. vkni
        02.08.2025 16:39

        Справа! Как подсказывает Hoogle — https://hoogle.haskell.org/?hoogle=Either a b -> b


  1. JordanCpp
    02.08.2025 16:39

    80% ошибок исключительные, просто runtime error и все. Все остальное это логика программы. А вы пытаетесь все обрабатывать неким единым ручным способом, в си оправдано, но в более высокоуровневых языках нет.


  1. DmitryOlkhovoi
    02.08.2025 16:39

    Скоро всем мы будем goto вода


  1. Naf2000
    02.08.2025 16:39

    Выделение метода при большой вложенности. Например уже двойной цикл желательно, а тройной уж точно


  1. MAXH0
    02.08.2025 16:39

    Картинка где готу сидит на диване и все другие операторы готовы его активно пользовать )))


  1. fixator10
    02.08.2025 16:39

    Во всех случаях, где мне надо вернуть ошибку, я использую паттерн Result.

    Я надеюсь вы не HTTP API пишете. А то встречались уже HTTP 200, смотришь внутрь, а там {"error": "not found"}


  1. greypo
    02.08.2025 16:39

    Читал что используют «встроенные функции» вместо goto для возврата значений. Где return error(); а там уже оттуда уже возвращается ошибка и данные…

    «Локальная функция»

    [httpPost]

    public IAct Submit(ship form){

    IAct error (){

    Response.Cookies.Append(“shipping’s error”,”1”);

    return RediAct(“Index”,”ship”,form);

    }


  1. ethien
    02.08.2025 16:39

    Все же throw это не совсем goto. Throw - это структурированный goto с понятными правилами куда он выкинет. Если идти дальше в этом направлении, то нужно забанить и return, поскольку return может выходить из середины функции или блока. Вместо этого все обмазывать флагами чтобы выход был только один в самом конце. Просто код с флагами будет хуже читаем, поэтому и делают return и throw. Это компромисс.


    1. vadimr
      02.08.2025 16:39

      В goto прямо явно написано, куда он выкинет, куда уж понятнее.


    1. Mausglov
      02.08.2025 16:39

      Вы будете смеяться, но когда-то я читал публикацию, где автор очень серьёзно топил за единственный return. Мол, если их много - то с кодом однозначно что-то не так. Я тогда даже проникся этой идеей - но было это много лет назад, я был зелен и глуп...


      1. mvv-rus
        02.08.2025 16:39

        Тем не менее, в отказе от преждевременных return есть свой резон: для них можно запросто забыть написать код, который прибирает за собой (например, делает Dispose, или освобождает блокировку). Но в C# всё это изначально лечилось тем, что такой код пишется в блок finally - и тогда он точно будет исполнен, даже если return будет сделан изнутри блока try. А ещё в более-менее современном C# есть для этого специальные операторы using и lock - с ними даже try...finally самому писать не надо.


      1. SpiderEkb
        02.08.2025 16:39

        Это делается ради единой точки выхода. Когда требуется подчистка выделенных локально ресурсов.


  1. mvv-rus
    02.08.2025 16:39

    Статья вызывает возражения.
    Во-первых, автор излишне драматизирует throw в заголовке, будто это тот же goto.
    На самом деле, тут есть существенная разница. Чтобы ее понять, нужно вспомнить о структурном программировании. Концепция эта старая (повека где-то уже), а потому я ее здесь напомню.

    Давным-давно, когда многие нынешние мидлы и даже некоторые сеньоры ещё не родились, возникла ип остепенно стала общепринятой идея,
    что программа, чтобы легко читаться, должна иметь определенную структуру передачи управления в ней - состоять из нескольких типов элементов (возможно, вложенных друг в друга). Эти элементы включают в себя, как минимум:
    простые операторы (включая оператор вызова подпрограммы), последовательности выполняемых один за другим одного или некольких операторов (иначе, блоков),
    условные операторы - блоки, выпаолняемых при определенных условиях, и операторов цикла - блоки, выполняемых неоднократно при соблюдении определенных условий. И, возможно - других операторов передачи управления, вызывающих преждевременное завершение блоков, которые эту структуру не нарушают, и могут быть заменены условными операторами: break, continue... Оператор вызова исключения throw тоже входит в число таких сохраняющих структуру операторов. А вот goto, который может передать управление откуда угодно куда угодно - в общем случае не входит. А потому throw!=goto

    Второе возражение - недостатки вызова исключений при высокой глубине вложенности вызовов

    В итоге получилось так, что вызывается некий метод, далее всё это сильно ветвится, вызывая прочие методы. И разные методы выбрасывают разные типы исключений, а ещё некоторые из них отлавливаются и просто логируются, а некоторые пробрасываются дальше. Некоторые нигде не отлавливаются, а некоторые перебрасываются с новым текстом ошибки.

    не спепецифичны именно для исключений - они, как мимнимум, те же, что и в шаблоне с результатом, включающим ошибку: объекты ошибок точно так же могут быть разного типа, некоторые ошибки могут точно так же обрабатываться внутри частично ("логироваться") или полностью (ещё и не передаваться дальше), заменяться другими - короче автора это всё равно бы не избавило от необходимости самостоятельно анализировать документацию или код промежуточных подпрограмм при ее отсутствии.

    Идея автора

    Моя идея проста: выбрасывайте исключения только там, где они действительно нужны.

    • тем не менееЮ правильная, но - по другой причине: существующие реализации обработки исключений обычно дорого обходятся. Это не везде и не всегда так: структрурная обработка исключений (SEH) в ядре Windows достаточно легка, потому поддерживается аппараторой (кстати, ради совместимости с SEH при переходе к x64 пришлось сохранить некоторые регистры, которые иначе для x64 нужны, как козе баян). Но в языках высокого уровня такая поддержка обычно не используется (ну, в Delphi 32-битной использовался SEH, но для нас это не актуально), поэтому совет экономить на throw остается.

    Короче, как-то так.

    PS IMHO куда шире, чем try...catch стоит использовать конструкцию try...finally, чтобы прибрать за собой, например - по-любому выполнить очистку (Dispose) объектов, которые такую очистку предусматривают. Или - по-любому освободить захваченную блокировку. Разработчики C#, похоже, тоже так же думают, и они для таких случаев ввели специфические конструкции (using и lock) прямо в язык.


    1. vadimr
      02.08.2025 16:39

      Вопрос очистки имеет очень разную значимость для разных языков. В языке с ручным управлением памятью вроде C++ очистка имеет первостепенную важность, а в языке с автоматическим управлением вроде Python - это скорее редкая ситуация. При чисто функциональном стиле вообще нет никакой очистки.


      1. mvv-rus
        02.08.2025 16:39

        Автор тут писал про конкретно C#, а в C#, несмотря на автоматическое управление памятью, есть резон очистку все-таки выполнять: GC будет меньше работы при сборке мусора, чем если бы ему самому пришлось вызывать Finalize. Ну и, если очистка возвращает в общее пользование какие-то разделяемые ресурсы (например, разрешения на параллельное выполнение от ConcurencyRateLimiter, я сейчас как раз в этой теме, потому что статью пишу), то эти ресурсы вернутся в общее пользование быстрее.


        1. vadimr
          02.08.2025 16:39

          Не думаю, что в общем случае имеет смысл облегчать руками автоматическую работу GC.


          1. mvv-rus
            02.08.2025 16:39

            ОК, не вижу смысла спорить с вашим глубоко личным мнением.


      1. SpiderEkb
        02.08.2025 16:39

        Речь может идти не о тех ресурсах, которые могут быть очищены автоматически, но о какой-то обязательной логике типа commit/rollback или отсылке куда-то какого-то результата в зависимости от того, что успели или не успели сделать в процессе выполнения.


        1. vadimr
          02.08.2025 16:39

          Всё верно, поэтому я и пишу, что ситуация редкая, а не невозможная.

          Хотя rollback (или commit) скорее всего будет выполняться автоматически при уничтожении соединения по таймауту запуска GC. Но тут многое зависит от прикладной логики.


          1. SpiderEkb
            02.08.2025 16:39

            А как эта сама автоматика узнает когда нужен rollback, а когда commit?

            А если вы обрабатываете некоторое сообщение из очереди и обязаны в очередь же отправить какой-то ответ в любом случае?

            Вам каждый return придется оборачивать блоком кода, который что-то делает по логике завершения.

            В нашем языке для этого можно объявить блок on-exit - туда безусловно попадешь в любом случае. Даже если случилось не перехваченное системное исключение (аварийный выход).


            1. vadimr
              02.08.2025 16:39

              Ну если логически рассуждать, то commit должен быть явным, а rollback - по умолчанию.


  1. sergey-kuznetsov
    02.08.2025 16:39

    вы иногда пиши́те try-catch

    — это звучит как призыв к действию, но вы, наверное, хотели написать другое:

    вы иногда пи́шете try-catch

    — просто констатация факта.


  1. Hheimerd
    02.08.2025 16:39

    У таких конструкций и паттернов есть фатальный недостаток, их нужно пробрасывать выше и выше. Каждая вложенная функция должна позаботиться об ошибке. Зачем все это если можно сделать обработчик ошибки по месту, где он нужен?

    Тейк про скорость очень сомнительный. Если не писать вообще весь код на исключениях, то разницы нет.


    1. withkittens
      02.08.2025 16:39

      У таких конструкций и паттернов есть фатальный недостаток,

      Сильно зависит от языка. Например, в Rust есть discriminated unions, #[must_use] и try operator. Вместе они делают работу с results удобной и одновременно убирают заботливо разложенные грабли (например, случайно проигнорировать ошибку нельзя). Win-win.

      В C# ничего этого нет, вот и выходит, что работать с исключениями гораздо проще. Но в целом, это не вина паттерна.


    1. SpiderEkb
      02.08.2025 16:39

      Разницы нет до тех пор пока не начнут строго спрашивать за производительность. А вот когда начнут, то удобнее сразу написать как эффективнее работает (а не как писать удобнее) чтобы не переделывать все после провала на нагрузочном тестировании - это точно неудобно с точки зрения разработки.

      Ну и избегать большой вложенности вызовов (и большой глубины стека) - это тоже хороший стиль.


      1. vvdev
        02.08.2025 16:39

        начнут строго спрашивать за производительность

        try-catch-finally/using практически бесплатны (если не помещать их внутри длинного цикла на горячем пути, конечно)

        throw дорогой, но - можно пример сценария, при котором throw начинает влиять на производительность нормального/expected случая, а значит и среднюю производительность?

        избегать большой вложенности вызовов (и большой глубины стека) - это тоже хороший стиль

        Более чем спорно


        1. SpiderEkb
          02.08.2025 16:39

          Как вы себе представляете работу try/catch без throw где-то внутри?

          throw дорогой, но - можно пример сценария, при котором throw начинает влиять на производительность нормального/expected случая, а значит и среднюю производительность?

          Да легко. Обработка миллиона записей (а это немного на самом деле, бывает и кратно больше) где могут случаться какие-то нештатные ситуации, которые нужно отложить для последующего ручного разбора и идти дальше.

          Особенно в случае, когда все это всего лишь один из тысяч работающих на сервере процессов.

          Тут не только общая производительность сервера может деградировать, тут еще нагрузка на сервер будет расти.


  1. levder
    02.08.2025 16:39

    Вы сейчас описали пример как работает голанг. Там полностью отсутствуют эксепшины и используется именно такой подход.

    Я согласен, что он имеет место быть и даже возможно иногда лучше чем экспешены. Но к сожалению вы забываете или не упоминаете в этой статье о том, чтобы использовать данный метод надо везде будет обложиться иф стейтментами.

    Если у нас большое кол-во вложенности и оч много методов возвращающих такой тупл - то надо везде обкладываться условиями и проверять не вернулась ли ошибка

    if (!validator.Validate(dto, ModelState)) return BadRequest(ModelState);

    И такой код должен быть в огромных кол-вах продублирован в приложении, что не шибко облегчает чтение кода(Именно так и происходит в большинстве голанг приложений.)

    А это противоречит вашей фразе

    Смотрите, как красиво, легко читается и предсказуемо выполняется!

    Не особо легко читается если всё приложение пестрит строчками if(blabla) return Response

    Я не фанат ни того ни другого подхода, но наверно стоит упомянуть и минусы такого подхода в статье. Не все так однозначно, к сожалению.


  1. zooh
    02.08.2025 16:39

    Но ведь именно для устранения бойлерплейта, связанного с обслуживанием Result, исключения и используют. Никакой семантической разницы между явным возвратом результата и "невидимым" пролетом исключения вверх по стеку нет, и нет проблемы, которую исключения создают, а Result решает.

    Что исключения, что результаты позволяют отделить бизнес-логику от логики обработки ошибок. Другое дело, что неопытные программисты часто не могут понять, где логика, а где ошибка - вот тогда и начинаются многоуровневые невидимые спагетти (причем использование Result их видимее не сделает). Я как-то столкнулся с полноценным конечным автоматом на разбросанных по всему коду исключениях - но проблема-то была не в механизме, а в его неуместном применении и в размазанности бизнес-логики.


    1. SpiderEkb
      02.08.2025 16:39

      Но ведь именно для устранения бойлерплейта, связанного с обслуживанием Result, исключения и используют.

      Не для устранения, а для "запихивания его поглубже под..."

      Другое дело, что неопытные программисты часто не могут понять, где логика, а где ошибка

      А ошибка часто является частью логики. Если это не тупая ошибка деления на ноль (например). И то, ноль может быть логически обоснованным и требующим отдельной ветки в логике.


      1. zooh
        02.08.2025 16:39

        Напротив, Result "запихивает поглубже": в исключении, как правило, есть информация о стеке, а в результате, как правило, нет.

        ошибка часто является частью логики

        В этом случае она обрабатывается непосредственно в момент возникновения и сложностей не создает. Существенной разницы между try/catch и if/else при этом не возникает.

        P.S. Такая ситуация возникает не "часто", а "редко" - потому исключения и прижились. Достаточно заглянуть в любой код на Go и посчитать, сколько раз ошибку вернули без обработки, а сколько - обработали.


        1. SpiderEkb
          02.08.2025 16:39

          Напротив, Result "запихивает поглубже": в исключении, как правило, есть информация о стеке, а в результате, как правило, нет.

          Это уже фактически проработанная ошибка. Там не нужна информация о стеке.

          Существенной разницы между try/catch и if/else при этом не возникает.

          Ровно до тех пор, пока не прогонишь реальную задачу с большой плотностью вызовов через профайлер