Новий оператор JavaScript, що повністю змінює правила гри

💡 Усі статті, обговорення, новини про Front-end — в одному місці. Приєднуйтесь до Front-end спільноти!

Привіт, мене звати Євген Русаков, я працюю в Сільпо на позиції техліда, і я радий поділитися з вами деякими новими можливостями, які з’явилися в JavaScript. У цій статті ми розглянемо потужний новий оператор, який спрощує обробку помилок та зменшує вкладеність коду. Ви дізнаєтеся, як він допомагає зберегти «незмінність змінних» і робить ваш код більш зрозумілим та легким для читання.

Ось ще один варіант:

Новий оператор безпечного присвоєння дозволяє уникнути написання коду таким способом:

// ❌ До:
// ❌ Глибоке вкладення try-catch для обробки різних помилок
async function loadUserProfile() {
  try {
    const response = await fetch('https://api.example.com/user');
    try {
      const userProfile = await response.json();
      return userProfile;
    } catch (jsonError) {
      console.error(jsonError);
    }
  } catch (fetchError) {
    console.error(fetchError);
  }
}

Тепер ви можете писати код у такий спосіб:

✅ Після:

async function loadUserProfile() {
  const [error, result] ?= await fetch('https://api.example.com/user');
  if (error) return console.error(error);
  const [jsonError, profileData] ?= await result.json();
  if (jsonError) return console.error(jsonError);
  return profileData;
}

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

Замість того, щоб мати справу з помилками у блоці catch, тепер ми можемо обробляти їх усього в один рядок. Це спрощує код, робить його більш читабельним та зручним для роботи, дозволяючи зосередитися на основній логіці без зайвих деталей.

async function fetchData() {
  const [err, data] ?= await func('https://api.sample.com/resource');
}

Також є можливість просто скіпнути помилку:

async function fetchData() {
  const [, data] ?= await func('https://api.sample.com/resource');
}

Або обробити його, якщо це потрібно, і продовжити рухатися далі:

async function fetchData() {
  const [err, data] ?= await func('https://api.sample.com/resource');
  if (err) {
    console.error(err);
  }
}

У разі необхідності, можна зупинитися:

async function fetchData() {
  const [err, data] ?= await func('https://api.sample.com/resource');
  if (err) return;
}

Це робить оператор таким потужним інструментом для створення гвардів:

function analyzeBooks() {
  const filePath = 'books.txt';
  
  // Використання синтаксису для обробки помилок при читанні файлу
  const [fileError, fileContent] ?= fs.readFileSync(filePath, 'utf-8'); 
  
  // Перевірка на помилки при читанні файлу
  if (fileError) {
    console.error('Failed to read the file:', fileError);
    return;
  }
  // Обробка JSON та перевірка на помилки
  const [parseError, jsonData] ?= JSON.parse(fileContent);
  if (parseError) {
    console.error('Error parsing JSON:', parseError);
    return;
  }
  // Підрахунок кількості книг
  const totalBooks = jsonData.books.length;
  console.log(`📚 Total books: ${totalBooks}`);
}

І ось одна з найкращих рис цього нового оператора.

Існує кілька випадків, коли нам потрібно значення, що залежить від наявності чи відсутності виключення.

Зазвичай ви використовуєте змінну, що змінюється, поза межами області видимості для безпомилкового доступу:

function saveTransactionData(transactions) {
  // Початковий стан
  let status = 'pending';
  try {
    // Запис транзакцій у файл
    fs.writeFileSync('transactions_log.txt', transactions);
    // Змінюємо стан на успіх
    status = 'success';
  } catch (error) {
    // Змінюємо стан на помилку
    status = 'error';
    console.error('Error saving transaction data:', error.message);
  }
  // Виводимо статус запису
  console.log(`Transaction save status: ${status}`);
}

Але це може бути дивним, особливо коли ви намагаєтеся мати незмінний код, а змінна вже була оголошена як const до того, як прийшов час додати try-catch.

Вам доведеться обгорнути його в try, потім видалити const, потім оголосити let поза блоком try, а потім знову переприсвоїти в блоці catch...

Але тепер із використанням ?= це виглядає так:

function saveTransactionData(transactions) {
  const [fileError] ?= fs.writeFileSync(
    'transactions_log.txt',
    transactions
  );
  
  const status = fileError ? 'error' : 'success'; 
  // робимо щось із status...
}

Ми зберігаємо нашу незмінність, і код тепер значно інтуїтивніший. Плюс до цього ми позбулися всіх вкладень

ℹ️ Поточні обмеження:

Наразі оператор безпечного присвоєння ?= все ще є пропозицією, яка обговорюється в рамках ECMAScript, і на момент написання статті не є частиною стандарту.

Назва оператора: Оператор ще не має чіткої назви, і це може викликати плутанину.

Блоки finally: Незважаючи на введення оператора ?=, блоки finally все ще залишаються важливими для завершення коду, і їх потрібно використовувати в традиційний спосіб.

Більше інформації можна знайти у репозиторії на GitHub.

Джерела:

Medium, Dev.to

👍ПодобаєтьсяСподобалось9
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Той випадок коли ти правовірний JS але хочеш бути схожим на Golang

Ох ті джаваскиптери, яких тільки костилів не вигадували за десятки років....

є просте рішення в рамках існуючого синтаксису:

async function loadUserProfile() {
  try {
    const response = await fetch('https://api.example.com/user');
    const userProfile = await response.json();
    return userProfile;
  } catch (jsonError) {
    console.error(jsonError);
  } catch (fetchError) {
    console.error(fetchError);
  }
}
лишилось його реалізувати.
лишилось його реалізувати.

І зламати весь існуючий код, що в екосистемі JS недопустимо (хоча тут не дуже зрозуміло як воно має однозначно маппитися на конкретний catch). Та й чи зайвий catch, чи як зараз if/switch/hash невелика різниця

я взяв семпл зі статті для наочності.
в ПХП мапиться по типам ексепшенів.

яким чином ламається існуючий код? відсутністю зворотної сумісності у конструкції?
а як щодо запропонованого в статті оператора?

в ПХП мапиться по типам ексепшенів.

Ну так в JS то немає ніяких типів і технічно будь що може бути експешненом навіть NaN чи null. Класи помилок не так активно використовують як коди, тому користі все рівно мало від фільтру по классу помилки, щоб щось там додавати в функціонал.

яким чином ламається існуючий код? відсутністю зворотної сумісності у конструкції?

Тут все повинно буди зворотно сумісним, тільки розширюватися, це ж веб.
Без інтерфейсів в JS тут же ніяк не реалізуєш фільтр по класу ексепшена при декларації, а без нього це зміна існуючої поведінки кетча. Хоча там же взагалі у вас не визначено якісь фільтри, просто два кетча куди що попаде не зрозуміло, але ж якась зміна буде.

async function loadUserProfile() {
try {
const response = await fetch(’api.example.com/user’)
const userProfile = await response.json();
return userProfile;
} catch (jsonError: SyntaxError) {
console.error(jsonError);
} catch (fetchError: {code: ECONNRESET}) {
console.error(fetchError);
} catch (err) {

}
}

Просто catch замість if.

а як щодо запропонованого в статті оператора?

Нижче у мене вже підгорало від нього 😁 Catch воно ніяк не замінює, за те буде мішанина і куча коду для прокидування експешена наверх. Це цілий оператор заради нішовго випадку, який закривається простою утилітою для асинхронного виклику (ну чи просто одним then)
Додавати в JS треба тільки саме необхідне, яке часто використовується (??, ?.) чи складно реалізовується на JS (або взгалі не реалізовується типу WeakRef), тому суттєво впливає на кодову базу. Все нове ж треба комусь в рушіях реалізовувати, включаючи обмеженні платформи IOT.
Наляпати подібне можна запросто, а от необхідність дуже сумнівна.

const aggregate = (fn) => {
  try {
    const res = typeof fn === 'function' ? fn() : fn;
    return res?.then ? res.then(v => [null, v], e => ([e])) : [null, res];
  } catch(err) {
    return [err];
  }
};

async function loadUserProfile() {
  const [error, result] = await aggregate(fetch('https://dummyjson.com/users'));
  if (error) return console.error('error', error);
  const [jsonError, profileData] = await aggregate(result.json());
  if (jsonError) return console.error('jsonError', jsonError);

  const [syncError] = aggregate(() => {
    profileData.notExists[0]++; // TypeError
  });

  if (syncError) return console.error('syncError', syncError);

  return profileData;
}
Вартує воно цілого оператора?

Дякую, ні. Десь хтось поміняє рядочки з await і весь try...catch коту під хвіст, бо він залежить на порядок виклику. Угумсь, кайф.

воно так в любій мові програмування буває: якщо залежні рядки поміняти місцями — код перестає працювати як очікувалось.

о він залежить на порядок виклику

Якщо воно по порядку фасуватиме, то це значить не можна буде безпечно використовувати умовні оператори, наче з хуками реакту 😁

я не джаваскріптер, але навіщо писати

async function loadUserProfile() {
  try {
    const response = await fetch('https://api.example.com/user');
    try {
      const userProfile = await response.json();
      return userProfile;
    } catch (jsonError) {
      console.error(jsonError);
    }
  } catch (fetchError) {
    console.error(fetchError);
  }
}

якщо можна написати так -> і уникнути зайвого рівня вкладеності?

async function loadUserProfile() {
  let response;
  try {
    response = await fetch('https://api.example.com/user');
  } catch (fetchError) {
    console.error(fetchError);
    return;
  }
  try {
    const userProfile = await response.json();
    return userProfile;
  } catch (jsonError) {
    console.error(jsonError);
  }  
}

а взагалі тут напрошується шоб фетч і парсінг в окремих методах були, якщо там реально різна логіка обробки ексепшена

async function loadUserProfile() {
  const response = await getResponse('https://api.example.com/user');
  const userProfile = getJson(response);
  return userProfile;
}

по теме — код в обох випадках виглядає не дуже, в першому варіанті багато вкладеності і немає декомпозиції. в другому нечитабельно взагалі, просто каша

В вечный флейм «коды ошибок против исключений» добавили дровишек.
В данном случае сторонники Go будут счастливы, ибо

if err != nil {
  return nil, err;
}
из JS тоже можно сделать Go, как мы успешно видим в данной статье.
я працюю в Сільпо на позиції техліда

уже звучит как анекдот
извините

кожного дня новий «рецепт» вирішення задач ;)

не звучить як анекдот, а звучить так «людина працює техлідом у великій рітейл корпорації»

async function loadUserProfile() {
const [error, result] ?= await fetch(’api.example.com/user’)
if (error) return console.error(error);
const [jsonError, profileData] ?= await result.json();
if (jsonError) return console.error(jsonError);
return profileData;
}

Ну і какофонія. Ну і що далі з цим return? Ловити на всіх аппер рівнях в if(returnValue instanceof Error) return;? Може й stdout і stderr в один запихнути? Круті чуваки придумали механізм обробки помилок з розрізненням даних від помилок в окремих флоу, іншу чуваки називають проривом примітивний анархізм, який був до цього.
Ну і цілий оператор, функціонал якого можна запихнути в один рядок утиліти, взагалі сумівно. Якщо все пхати в мову, то буде знову як пхп загажений 100500 схожими функціями.

Обработка ошибок в try-catch блоках всё равно сводится к проверкам условий типа if (error). Отсюда возникает вопрос — и к чему тогда этот огород с блоками try? Эффективнее правильно структурировать результаты, то есть возвращать 2 или несколько параметров, где последний — объект ошибки, и сразу делать проверку.

Огород і не потрібен, якщо мати трохи розуміння що ти взагалі пишеш. Upper-level catch ловить все що всралось знизу. Якщо треба десь всередині спіймати, от тоді вже городиш catch in between. ?= прикольна штука, але вона ніколи try/catch не замінить.

В ноді умовні круди мають по 4-5 леєрів, оброблювати помилки через ?= в кожному з них це щось з розряду «оплата за кількість коду». Один try/catch на найвищому рівні повністю справляється з цим всим. Ну і звичайно локальні try/catch за потреби.

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

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

к проверкам условий типа if (error).

Тепер ще одна перевірка чи взагалі воно помилка, бо на якомусь етапі викликів ми ламаемо механізм try-catch і далі як дикуни ручками розгрібаємо де мухи, де котлети. Круто.

Эффективнее правильно структурировать результаты

Це не ефективніше, а рівень хелло ворлд для самих маленьких

то есть возвращать 2 или несколько параметров

Щось подібне було в колбек хелі, але щось мало кому сподобалось і зробили знову ж таки двофлоуну обробку асинхронних викликів на промісах, щоб вона відповідала синхроному механізму try-catch-finally.

, и сразу делать проверку.

А як відразу не потрібно? Вам треба обробити помилку в функції, яка сталася десь в глибоко стеці викликів, десь там ще звільнити ресурси, відкотити стейт, закрити асинхронний ітератор.
Всюди перевіряти if(error){doThat();throw new Error()} це люта хрінь, а не прорив порівняно з нормальним трай-кетч-фіналлі, ще й куча зайвих змінних щоб ловити помилки, які тобі catch сам принесе з будь-якого рівня стека викликів.

Новий оператор JavaScript, що повністю змінює правила гри

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

Обізяни з go вирішили що їх банан найсмачніший? :))

Ммм, клааас, люблю коли перекладають статті з медіуму не вказуючи оригінал (так, переклад не дослівний, але зразки коду — 1 в 1, що натякає на першоджерело)

medium.com/...​ipt-operator-1e60dea05654

Дякую що підсвітили, додав джерела

Дуже не хочеться бути душним, але:
— З тієї інформації, що я знайшов, це лише пропозал на ранній стадії, тобто не частина стандарту, а відповідно й не Новий оператор JavaScript
— Посилання на джерела інформації дуже вітаються, щоб ось такі душніли як я не витрачали час на пошуки цих джерел, щоб вказати на той факт, що це лише пропозал на ранній стадії, тобто не частина стандарту, а відповідно й не Новий оператор JavaScript

Дякую за зауваження! Так, наразі оператор безпечного присвоєння (?=) все ще є пропозицією, яка обговорюється в рамках ECMAScript, і на момент написання статті не є частиною стандарту.

Підписатись на коментарі