JavaScript. Работаем с исключениями и данными в конструкциях async/await без блоков try-catch

https://dev.to/sadarshannaiynar/capture-error-and-data-in-async-await-without-try-catch-1no2
  • Перевод
Появившиеся в JavaScript новые асинхроные конструкции async/await выглядят проще, чем Promise, и, конечно, значительно читабельнее, чем «callback-джунгли». Но одна вещь беспокоила меня — это использование try-catch. Сначала я подумал, что это не проблема, но, к несчастью, мне пришлось работать с цепочкой вызовов API, в которой каждый вызов API имел свое сообщение об ошибке, которое должно было прологировано. Вскоре я понял, что создаю «try/catch-джунгли», которые ничем не лучше «callback-джунглей».

Давайте рассмотрим этот Promise, который возвращает данные или исключение через 2 секунды в зависимости от параметра rejectPromise:

// api.js
const fetchData = async (duration, rejectPromise) => (
  new Promise((resolve, reject) => {
    setTimeout(() => {
      if (rejectPromise) {
        reject({
          error: 'Error Encountered',
          status: 'error'
        })
      }
      resolve({
        version: 1,
        hello: 'world',
      });
    }, duration);
  })
);

module.exports = {
  fetchData,
};

Типичное использование этой функции будет выглядеть так:

const { fetchData } = require('./api');

const callApi = async () => {
  try {
    const value = await fetchData(2000, false);
    console.info(value);
  } catch (error) {
    console.error(error);
  }
}

callApi();

/* 
 OUTPUT: 
 { version: 1, hello: 'world' } (rejectPromise=false)
 { error: 'Error Encountered', status: 'error' } (rejectPromise=true)
 */

Как вы можете видеть, когда параметр rejectPromise является ложным, Promise возвращает значение {version: 1, hello: 'world'}, а когда оно истинно, он вызывает исключение { error: 'Error Encountered', status: 'error' }.

Это типичная реализация async-await. Теперь мы попытаемся использовать Promise, чтобы сделать этот код более простым. Давайте напишем функцию-обертку, которая упростит нам обработку исключений.

// wrapper.js
const wrapper = promise => (
  promise
    .then(data => ({ data, error: null }))
    .catch(error => ({ error, data: null }))
);

module.exports = wrapper;
</source
Мы можем видеть, что функция-обертка принимает Promise в качестве входного параметра и возвращает ошибку или даные с использовнаием конструкции then().catch(). Итак, давайте изменим исходный код с применением функции-обертки:

<source lang="javascript">
const { fetchData } = require('./api');
const wrapper = require('./wrapper');

const callApi = async () => {
  const { error, data } = await wrapper(fetchData(2000, false));
  if (!error) {
    console.info(data);
    return;
  }
  console.error(error);
}

callApi();

/* 
 OUTPUT: 
 { version: 1, hello: 'world' } (rejectPromise=false)
 { error: 'Error Encountered', status: 'error' } (rejectPromise=true)
 */

image — получаем тот же результат но более читабельным кодом.

Полезные ссылки.


1. Почему [не] надо использовать async/await
Поделиться публикацией

Похожие публикации

Комментарии 24
    +3
    Так и не понял. Зачем функция из первого примера помечена как async, если внутри нигде не вызывается await?
      +2
      А потом кто-то забывает обработать error, и вот, привет игнорирование исключений в самых неожиданных местах.

      И да, толку от обработки вида `console.error(err)` очень мало. Может, вовсе не стоит ловить исключение, если оно обрабатывается таким образом?
        –3
        Есть одна неприятная вещь в промисах равно как и в асинках. Если не будет обработан reject то nodejs завершит работу с ошибкой. Это по сути обязывает разработчика все конструкции await заключать в блоки trycatch. Поэтому способ предложенный автором снимает определённую часть проблем.
          +2
          Поэтому способ предложенный автором снимает определённую часть проблем.

          Автор утверждает что: «try/catch-джунгли» ничем не лучше «callback-джунглей».

          И он прав, но предложенное решение это «if/else-джунгли». В этом плане все перечисленные решения одинаковы, так как являются вариацией одного и того же явления: "Pyramid of doom"
            0
            Из callback-джунглей был предложен хороший выход github.com/creationix/step. Я так ни разу не воспользовался этой библиотекой т.к все быстро перешли на промисы. Хотя с учётом того что промисы стали наличными и быстрыми не сразу указанное решение могло бы упростить разработку
              +1
              Более популярна для callback-джунглей библиотека www.npmjs.com/package/async
              В ней и функциональности больше, да и API на мой взгляд чище.

              Кроме того есть ее порт для промисов — www.npmjs.com/package/async-q
              Также полезна, так как организация высокоуровневых шаблонов асинхронности(ограничение количества потоков, очереди) в промисах все-таки отсутсвует
            +3
            обязывает разработчика все конструкции await заключать в блоки trycatch
            А нельзя сделать один trycatch где-то наверху, там где реквест/задача начинается?
              –3
              Единственный вариант — это в каждой асинхронной функции взять все в try/catch блок. Если это сделать где-то на самом верхнем уровне вызовов функции — то я сталкивался с ситуацией что это не помогает. Нужно будет специально исследовать этот вопрос.
                +1
                Нужен конкретный пример, где один внешний try-catch не работает
                Проверил на базовых примерах — внешний try-catch прекрасно работает

                Может сломаться, если не await-ить внутренние вызовы, но тут и try-catch-hell не спасет
                Как следствие может сломаться на изощренных юзкейсах, вроде «послать все запросы в цикле, их промисы запомним в массив, а потом в однопоточном цикле их await-ить и обрабатывать»
                Но в таких нетривиальных случаях пожалуй лучше будет использовать библиотеки управления потоком, или как workaround, вызвать перед циклом
                await Promise.race(results)


                Как следствие try-catch-и можно размещать согласно логике приложение(там где вы хотите реально обработать ошибку), например на уровне обработчика запроса — точно также, как это делается для синхронного кода
                  0
                  Я когда писал комментарий не помнил точно где это встречалось. Потом проверил. На уровне модуля то есть не внутри функции нельзя использовать конструкцию await. За этот факт и за необходимость явно определять функцию как async есть справедливая критика. То есть вызов на верхнем уровне асинхронного кода в trycatch невозможен. Вернее он не будет ловить ошибку так как нельзя использовать await
                    0
                    В таком случае можно обернуть верхний уровень в авто-вызываемую асинхронную функцию, и обработать ошибку в нем
                    (async function main() {
                        await func1();
                        await func2();
                    })().catch((err) => console.error(err))
                    


                    Также, вероятно на верхнем уровне асинхронные функции вызываются синхронно, одна за другой, потому что должны идти параллельно и независимо (иначе они уже были бы обернуты в асинхронную функцию, либо цепочку промисов).
                    Тогда стоит рассматривать эти функции как верхнеуровневые, и достаточно добавить try-catch только в них, либо вообще обработать их ошибки на уровне модуля
                    func1();
                    func2();
                    async function func1() {
                      try {
                        // ...
                      } catch(e) {
                        console.log(e);
                      }
                    }
                    async function func2() {
                      try {
                        // ...
                      } catch(e) {
                        console.log(e);
                      }
                    }
                    

                    func1().catch(e => console.log(e));
                    func2().catch(e => console.log(e));;
                    async function func1() {
                      // ...
                    }
                    async function func2() {
                      // ...
                    }
                    


                    В итоге try-catch все еще не нужен в каждой асинхронной функции, и мы получаем обработку ошибок на верхнем уровне(и в тех функциях где она нужна по логике приложения)
                    При этом, если исключения доходят до верхнего уровня, то логично использовать uncaughtException/rejectionHandled
                      0
                      Да все верно. Единственное что хотелось бы получать более простой для восприятия код. В этом смысле await как кажется на первый взгляд сильно все упрощает. Но когда начинаешь работать с конкретной задачей то сталкиваешься с необходимостью обрабатывать ошибки. С учётом появления стрелочных функций которые упростили код с промисами я бы сказал что по восприятию код с промисами и стрелочными функциями и код с async/await примерно равны по простоте/сложности для восприятия.
          +2
          получаем тот же результат но более читабельным кодом

          В статье не хватает определения читабельности.

          Примеры недостаточно показывают разницу, так как, положительный пример имеет:
          • Одинаковое количество строк кода в теле функции как и «try/catch» пример (6 строк)
          • Одинаковое количество условий выхода из функции («return;/console.error(error)» и «try/catch»)
          • Отрицательное условие: «если не ошибка» (if (!error))
          • Двойную интерпретацию «wrapper(fetchData(2000, false))», в зависимость от понимания работы «fetchData»:
            • «fetchData» — синхронна и возвращает данные на обработку в «wrapper»:
              const dataRaw = fetchData(2000, false);
              const data = await wrapper(dataRaw);
            • «wrapper» — модифицирует «Promise» объект, возвращаемый вызовом «fetchData»:
              const fetchPromise = fetchData(2000, false);
              const data = await wrapper(fetchPromise);


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

            Обратите внимание на вот такой подход: https://m.habr.com/post/339606/. Все try/catch уйдут, но обработка ошибок останется

              +11
              const { error, data } = await wrapper(fetchData(2000, false));
              if (!error) {

              Что-то мне это напоминает… :)
              image
                0
                Я заодно оставлю здесь более удобный способ создавать промисы.
                Код
                function Defer(){
                    var status;
                    this.resolve = function(value){
                        status = {
                            type: 'resolved',
                            value: value
                        };
                    };
                    this.reject = function(value){
                        status = {
                            type: 'rejected',
                            value: value
                        };
                    };
                
                    var that = this;
                
                    this.promise = function(){
                        return promise = new Promise((resolve, reject) => {
                            if(status){
                                if(status.type === 'resolved'){
                                    resolve(status.value);
                                } else {
                                    reject(status.value);
                                }
                            } else {
                                that.resolve = function(value){
                                    resolve(value);
                                }
                                that.reject = function(value){
                                    reject(value);
                                }
                            }
                        });
                    };
                }


                С ним можно делать более читаемый код:
                const fetchData = (duration, rejectPromise) => {
                    var defer = new Defer();
                
                    setTimeout(() => {
                        if(rejectPromise){
                            defer.reject({
                                error: 'Error Encountered',
                                status: 'error'
                            });
                        } else {
                            defer.resolve({
                                version: 1,
                                hello: 'world',
                            });
                        }
                    }, duration);
                
                    return defer.promise();
                }
                  0
                  Чем он более удобнее?, это стиль «deferred», от которого все* отказались много лет назад и перешли на промисы, в котором меньше кейвордов нужно помнить.
                    0
                    Тем, что не нужно создавать 20 вложенных друг в друга функций.
                    Лично мне наоборот сложнее, когда функций больше. У них ещё разные контексты исполнения, разные this (arrow functions не везде), где-то его нужно передавать, где-то нет… опять callback hell, в общем.
                      +1

                      Откуда вы взяли цифру 20? new Promise добавляет только одну дополнительную функцию в код, зато вы получаете важное преимущество — синхронно выброшенное исключение перехватится и зарежектит промис. Вы гарантированно никогда не получите синхронного исключения.

                        0
                        Число 20 появилось из традиционной для русского языка гиперболы.

                        Про исключения не знал. Тогда всё становится понятнее, окей.
                        0
                        Тем, что не нужно создавать 20 вложенных друг в друга функций.
                        Это наверное при каком-то неправильном использовании, есть пример? Промисы можно более менее линейно «стыковать».
                    +1

                    На самом деле, для подхода, описанного в статье, необязательно даже wrapper писать. Достаточно резолвить промис с ошибкой вместо режекта.


                    const fetchData = async (rejectPromise) =>
                      new Promise(resolve => {
                        if (rejectPromise) {
                          resolve({ error: "Error Encountered" });
                        } else {
                          resolve({ data: { version: 1, hello: "world" } });
                        }
                      });
                    
                    // использование будет таким же
                    const { error, data } = await fetchData(true);

                    Разумеется, это будет работать только для вашего кода, для библиотек нужна обертка.

                      +1

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

                        0
                        Такая идея заимствована из GO и уже давно реализована. Есть github.com/scopsy/await-to-js, и мой пакет github.com/ymatuhin/flatry который я писал когда еще не знал про существование await-to-js.

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

                        Самое читаемое