Pull to refresh

Comments 43

Уже и присваивание стало опасным! Ужас какой!

Как в анекдоте:

Вот никому нельзя доверять! Никому! Даже себе доверять нельзя!

...

Там смысл в том что все пошло не так.

// Традиционный подход
try {
  const response = await fetch("https://api.example.com/data");
  const json = await response.json();
  const data = parseData(json);
} catch (error) {
  handleError(error);
}

// С использованием ?=
const [fetchError, response] ?= await fetch("https://api.example.com/data");
if (fetchError) {
  handleFetchError(fetchError);
  return;
}

Потрясающе, с использованием уникального оператора отпадает необходимость получать json и парсить данные! Или дело в том, что если честный код приводить, то размер примеров будет одинаковым? Или, точнее, размер примера с try catch будет меньше, за счет того, что не надо делать проверки на каждый чих

  const [jsonError, data] ?= await response.json();
  if (jsonError) {
    handleJsonError(jsonError);
    return;
  }

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

Хотя это просто ещё один способ делать обработку ошибок.

Ты видишь чтобы кто-то принуждал тебя к использованию этой фичи? Они эти принудители здесь с тобой в одной комнате или ты просто захотел потешить своё ЧСВ разработчика?

Кстати я буду откровенен и честен с собой, я потешил спасибо.

Потому что обработка ошибок через возвращаемые значения это try catch "для бедных", это как рекламировать колбеки вместо async await. Годятся разве что для хитрых языков с автоматической параллелизацией последовательного кода, или для раста, где просто не смогли подружить свой компилятор с человеческой обработкой ошибок.

Единственное применение в js которое вижу: вызов методов которые могут фейлить, но нам нет до этого дела, например вызов логгера.

Ты видишь чтобы кто-то принуждал тебя к использованию этой фичи?

Вижу как сейчас пиарят фичу, а нам потом иметь дело с таким кодом на проектах или в api внешних библиотек.

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

let result

try {
  result = getSome()
} catch (e) {
  result = null
}

doSome(result)
const [e, result] ?= getSome()

doSome(result)

Тут скорее хочется чтобы блоки делали возврат

const result = try {getSome()} сatch {null}; 
//или объявление переменных внутри условий
if (const data = getSomeData()) {
  process(data)
} else {
  handleEmpty();
}

Вот объявления переменных в условиях очень хочется.

И еще бросать исключения в тернарниках и ??

const r = a === b ? ‘result1’ : throw new Error();
const r = a ?? throw new Error();

обойти блочную область видимости, частенько вот эти 'let' подбешивают

var ?

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

Тогда не нужно обходить блочную область видимости.

А что в расте не так с обратной ошибок?

Это как с Perl который дает 7 вариантов одного и того же функционала.

Вроде и "дают новую прикольную фичу" и "это просто еще один способ с делать" и вроде никто не принуждает.

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

Ты читать умеешь? Пример кода "с фичёй" - нерабочий.

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

Оберните в try-catch каждую строку отдельно и вызывайте те же самые handle...Error(e). Будет практически такой же код, что и с "безопасным" присваиванием.

В 99% случаев мы не хотим реагировать по разному, мы просто логируем ошибку и уходим. В 1% случаев мы проверяем на 1 конкретный тип ошибок вроде "aborted". И примерно в 0% мы реально обрабатываем каждый тип ошибок прямо на месте. Даже если мы в этом самом 0%, то код будет не длиннее проверки возвратов, и, вероятно, даже короче. Кстати, раз уж речь про типы зашла, хочется посмотреть на обработку множества типов ошибок потенциально возвращаемых из одного метода, особенно на typescript.

А что же вы в примере пункта 3 остановились на полпути? Давайте распишем его до конца:

// Традиционный подход
try {
  const response = await fetch("https://api.example.com/data");
  const json = await response.json();
  const data = parseData(json);
} catch (error) {
  handleError(error);
}

// С использованием ?=
const [fetchError, response] ?= await fetch("https://api.example.com/data");
if (fetchError) {
  handleFetchError(fetchError);
  return;
}

const [jsonError, json] ?= await response.json();
if (jsonError) {
  handleJsonParseError(jsonError);
  return;
}

const [dataError, data] ?= parseData(json);
if (dataError) {
  handleDataParseError(dataError);
  return;
}

Стало удобнее? Не факт.

Не забудьте еще каждый handle* метод расписать.

Во-первых, у вас в версии с ?= три разных обработчика ошибок, а не один, и try-catch версия была бы сильно длиннее. Во-вторых, снижение вложенности полезно тем, что вы как программист при отладке можете просматривать меньший объём кода: вместо одного большого блоба, где "что-то пошло не так" у вас явная последовательность действий, в которой разваливается конкретное ветвление. Если же, наоборот, мы упростим обработчики до return false, то будет очень компактно, хотя и менее информативно.

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

снижение вложенности полезно тем, что вы как программист при отладке можете просматривать меньший объём кода

Вы можете заметить, что вложенность обоих примеров одинаковая. А вот размер кода в высоту растет линейно. И делает простейший метод из 3х строк трудночитаемым. А если вызовов будет не 3 а 10, да еще и с 3х уровневой логикой?

Если же, наоборот, мы упростим обработчики до return false, то будет очень компактно, хотя и менее информативно.

Будет все еще минимум +1 строка на каждый обработчик, а с вменяемым стилем форматирования +4.

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

Всё верно, но чем концептуальная сложность проверки на ошибку каждого вызова в стиле вызова api методов 30 летней давности, меньше чем у try catch? Говоря о сложности, вам еще нужно придумать имя переменной для ошибки в каждом вызове, а GC создать и удалить массив.

Я уже ответил: сложность меньше тем, что вы контролируете каждое ветвление, а не ловите всевозможные ошибки из блока.

Про GC лучше даже не заикаться в контексте исключений. Исключения -- это дорого, там буквально для throw создаётся целый объект исключения, и производительность там будет проседать значительно, если использовать это под высокой нагрузкой. Куда сильнее, чем создание какого-то там массива на два элемента.

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

Я уже ответил: сложность меньше тем, что вы контролируете каждое ветвление, а не ловите всевозможные ошибки из блока.

Ничего не понял. Можно, пожалуйста, наглядный пример с меньшей сложностью?

Множество массивов для результатов создаются каждый вызов функции.

Это всё хорошо оптимизируется, и влияние на результирующую скорость исполнения здесь околонулевое.

Я воздержусь от продолжения бесполезного флуда.

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

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

А теперь напишите такую же функциональность на try/catch. чего же вы остановились...

Я что-то упустил, ее подвезли уже или только планируют?

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

Собственно то за что не нравиться Go пытаются затащить в Js. Тут где-то рядом целый трэд был про использование exception.

// С использованием ?=

const [fetchError, data] ?= parseData(await fetch("https://api.example.com/data").json());
if (fetchError) 
{  
  handleFetchError(fetchError);  
  return;
}

А что возвращает await fetch("https://api.example.com/data") ?

В примере без ?= и с ?= как будто разное...

Если await fetch возвращает response, а обработка исключений идёт в операторе безопасного присваивания, то надо писать

const [fetchError, data] ?=
  parseData(await (await fetch("https://api.example.com/data")).json());

Иначе получаем Promise.json(), вызов несуществующей функции.
Но это означает, что фактически делается двойная работа. fetch выбрасывает исключение, оно неявно перехватывается в операторе, который создаёт массив, заполняя его... А чем, кстати, будет заполняться поле ошибки в этом массиве? Ведь никто не мешает нам написать throw new Error(null);.

Мне кажется, что главный затык всех этих примеров модного и молодежного синтаксиса в том, что с точки зрения конечного пользователя случается "опаньки!". Все сломалось. Данные не грузятся. И мы можем сколько угодно обрабатывать ошибки каждого шага по отдельности, красиво их оформлять в коде, но в конечном счете ничего не работает. Этот компонент не может загрузиться. Лучшее, что мы можем сделать - это выключить и включить. Попробовать повторить всю процедуру с начала. Собственно в примерах, которые автор в предложении приводит - при любой ошибке мы уводим поток выполнения в handleAndDie(), проходим точку невозврата, не предполагая возможности горячего восстановления. А если нет возможности восстановиться, то в чем смысл всего этого огорода? Можно и один большой try-catch сделать с тем же эффектом. Было бы здорово в обсуждения таких инициатив добавить примеров, как все это будет выглядеть, если мы на самом деле будем обрабатывать ошибки, а не просто делать лапки разными способами.

То есть if (err != nil)?

Т.е. if(!!err)

Эх, предложили бы сразу монады Result и Maybe. И еще pattern matching с ними сразу бы завезли для их обработки красиво. А там и до Railway oriented programming рукой подать =)

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

почему не сделать так?

Было

async function readData(filename) {
    try {
        const fileContent = await fs.readFile(filename, "utf8")

        try {
            const json = JSON.parse(fileContent)
    
            return json.data
        } catch (error) {
            handleJsonError(error)
            return
        }
    } catch (error) {
        handleFileError(error)
        return
    }
}

Стало:

async function readData(filename) {
    try {
        const fileContent = await fs.readFile(filename, "utf8");
        const json = JSON.parse(fileContent);
        return json.data;
    }
    catch (error) {
        if (error instanceof FileError) {
            handleFileError(error);
        }
        else if (error instanceof JsonError) {
            handleFileError(error);
        } 
        else {
            throw new Error("Unknown error", { cause: error }); // rethrow
        }
    }
}

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

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

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

Основной мотивацией для введения оператора безопасного присваивания является стремление упростить обработку ошибок и улучшить читаемость кода

  • При том читаемость вызываемой функции - под вопросом, из-за бойлерплейста с Symbol.result.

  • Как избежать замусоривания кодом проброса ошибок выше (? из Rust не завезли)?

  • Как быть с функциями, которые бросают несколько разных (типов) ошибок, чтобы обрабатывать их по-своему, а какие-то игнорировать? Тут даже у try-catch не все ладно.

  • Явность обработки - это прикольно, но если функцию, возвращающую ошибки, вызывать с обычным присваиванием (=), будет сразу ошибка (как была бы при необработанном исключении) или она дрейфует дальше?

Sign up to leave a comment.