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

Типичное обещание JavaScript выглядит примерно так:

let x;
doVerySlowThing()
  .then(result => x = result)
  .catch(err => console.log('Oh noes!! Error: ', err.code));
doAnotherThing(x);

Конечно, это не сработает. Как только вызывается doVerySlowThing(), сразу же выполняется doAnotherThing(x), но ветвь then промиса сработает только позже, когда doVerySlowThing() завершит свою очень медленную работу. К тому моменту наш скрипт уже упал с исключением.

Решением может быть перемещение doAnotherThing(x) в ветку then(). Это действительно правильный способ использования Promise:

doVerySlowThing()
  .then(result => doAnotherThing(result))
  .catch(err => console.log('Oh noes!! Error: ', err.code));

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

doVerySlowThing()
  .then(result => 
     doAnotherThing(result)
       .then(result2 => 
          doAgainAnotherThing(result2)
            .then(result3 => console.log('Result:', result3))
            .catch(err => console.log('WTF?', err.code))
       )
       .catch(err => console.log('Failed!', err.code))
  )
  .catch(err => console.log('Oh noes!! Error: ', err.code));

В таком беспорядке всплывать ошибки в один обработчик ошибок в основной ветке catch() также может быть сложно, потому что разные функции могут генерировать разные объекты ошибок: одна может использовать обычный JavaScript, другая может быть ошибкой Axios с собственным форматом, затем служба PostgreSQL также может иметь другую и т.д. Наступает беспредел!

Итак, возникает вопрос: Как дождаться полного завершения промиса, включая его ветки then() и catch()?

Если вы думали, что это async / await, вы правы на 50%. Остальные 50% — это еще одно Обещание.

let x;
(async () => {
  await new Promise((resolve, reject) => {
    doVerySlowThing()
      .then(result => {
         x = result;
         resolve();
      })
      .catch(err => { throw err });
  })
  .catch(err => {
     console.log('Oh noes!! Error: ', err.code)
  });
})();
doAnotherThing(x);

Итак, что здесь происходит? Теперь doVerySlowThing() обернут другим промисом, который мы resolve() когда полностью закончим, включая ветку then().

Этот промис заключен в анонимную функцию async, чтобы мы могли вызвать для него await, что означает, что выполнение будет остановлено до тех пор, пока промис не разрешится. Поскольку doVerySlowThing() находится внутри этого Обещания, мы можем не беспокоиться об этом. Асинхронная функция выполняется автоматически (это то, что делают пустые скобки в конце), то есть вам не нужно ее вызывать.

Ветвь catch() doVerySlowThing() теперь генерирует исключение, которое перенесет объект err в ветвь catch внешнего промиса. Вот как мы можем обрабатывать ошибки внутри с помощью общего обработчика ошибок.

Основной промис имеет только ветку catch() и не имеет then() или finally(). Ни в том, ни в другом нет необходимости, потому что мы остановили выполнение скрипта. Что бы ни было после асинхронной функции, она будет работать только тогда, когда промис полностью завершится.

Я знаю, что есть много фанатиков «правильного пути», которые завопят от ужаса, увидев такой «неправильный» способ использования промисов. Что, установить переменную перед выполнением и использовать ее после?! Богохульство!! Да, именно это здесь и происходит. Нет, это не "неправильно". Это отличный и практически единственный способ упростить сложный каскад промисов. Вы можете выстроить столько из них, сколько пожелаете, внутри асинхронной функции, обернуть каждую из них во внешний промис с ключевым словом theawait и увидеть хорошо выполненную синхронную серию выполнения.

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