Promise — это удобный способ для обработки асинхронных операций в JavaScript, который помог нам уйти от «callback hell» в нашем коде. Однако многие разработчики не до конца понимают, что происходит у промисов под капотом и используют их неправильно, теряя все преимущества этой прекрасной технологии.

Давайте разбираться, какие ошибки встречаются особенно часто и как их можно избежать.

Ошибка #1. Promise Hell

Убегая от «ада коллбэков», мы иногда попадаем в «ад промисов»:

userLogin('user').then(function(user){
    getArticle(user).then(function(articles){
        showArticle(articles).then(function(){
            // ...
        });
    });
});

В этом фрагменте сразу три промиса, вложенных друг в друга: userLogin, getArticle и showArticle. Сложность растет с каждой строчкой, и этот код уже довольно сложно читать. По сути мы обменяли шило на мыло, используя callback-style с промисами.

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

userLogin('user')
  .then(getArticle)
  .then(showArticle)
  .then(function(){
       // ...
});

Ошибка #2. try/catch внутри промиса

Блок try/catch нужен для обработки ошибок, но использовать его внутри промиса — плохая идея. В этом просто нет необходимости. Если внутри произойдет что-то нехорошее, Promise автоматически обработает это сам.

new Promise((resolve, reject) => {
  try {
    const data = doThis();
    // ...
    resolve();
  } catch (e) {
    reject(e);
  }
})
  .then(data => console.log(data))
  .catch(error => console.log(error));

Все ошибки (даже опечатки) внутри тела промиса будут замечены самим промисом и перенаправлены в обработчик из метода catch. В результате мы получим отклоненное обещание (rejected).

new Promise((resolve, reject) => {
  const data = doThis();
  // do something
  resolve()
})
  .then(data => console.log(data))
  .catch(error => console.log(error));

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

Ошибка #3. Асинхронные функции внутри промиса

Синтаксис Async/Await — это очень удобный способ работать с промисами в синхронном стиле.

Если мы используем перед объявлением функции ключевое слово async, то результат, который она возвращает, будет обернут в промис. При вызове такой функции мы можем использовать ключевое слово await, чтобы остановить выполнение кода до тех пор, пока этот промис не будет выполнен (или отклонен).

Но если вы помещаете асинхронную функцию в промис, то появляются некоторые неожиданные эффекты.

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

Даже если вы используете блок .catch() или дождетесь выполнения промиса внутри конструкции try/catch, то все равно не сможете сразу же обработать ошибку.

// Ошибка не будет обработана

new Promise(async () => {
  throw new Error('message');
}).catch(e => console.log(e.message));

(async () => {
  try {
    await new Promise(async () => {
      throw new Error('message');
    });
  } catch (e) {
    console.log(e.message);
  }
})();

Объединяя async-функцию с Promise, мы пытаемся вынести асинхронную логику из обещания, оставив его синхронным. Это работает, но не всегда.

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

new Promise(async (resolve, reject) => {
  try {
    throw new Error('message');
  } catch (error) {
    reject(error);
  }
}).catch(e => console.log(e.message));


// с async/await
(async () => {
  try {
    await new Promise(async (resolve, reject) => {
      try {
        throw new Error('message');
      } catch (error) {
        reject(error);
      }
    });
  } catch (e) {
    console.log(e.message);
  }
})();

Ошибка #4. Немедленное выполнение промиса после создания

const myPromise = new Promise(resolve => {
  // код самого HTTP-запроса
  resolve(result);
});

В этом фрагменте мы поместили HTTP-запрос внутрь промиса.

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

somePromise().then(myPromise);

Но это не так. Функция, переданная в конструктор Promise, выполняется сразу же. То есть ваш HTTP-запрос запускается сразу же после создания обещания. В этом легко убедиться:

const myPromise = new Promise(resolve => {
  console.log('start request');
  // код самого HTTP-запроса
  resolve(result);
});

В консоли сразу же появится сообщение о начале выполнения запроса, хотя вы не вызывали myPromise.

Что же делать, если вы хотите отложить выполнение коллбэка промиса — выполнить запрос только тогда, когда он будет вам нужен.

У самих промисов нет встроенного способа сделать это, однако вы всегда можете обратиться к нативным методам JavaScript. Если нет способа отложить выполнение коллбэка промиса, то просто отложите создание этого промиса.

const createMyPromise = () => new Promise(resolve => {
  // HTTP-запрос
  resolve(result);
});

Это «ленивый» промис.

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

Ошибка #5. Не использовать Promise.all при необходимости

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

Для этого существует метод Promise.all().

Пример последовательного выполнения:

const sleep = delay => new Promise(res =>  setTimeout(res, delay))

async function f1() {
  await sleep(1000);
}

async function f2() {
  await sleep(2000);
}

async function f3() {
  await sleep(3000);
}

(async () => {
  console.time('sequential');
  await f1();
  await f2();
  await f3();
  console.timeEnd('sequential');  
})();

Этот код выполняется 6 секунд!

Выполнение последовательных вызовов заняло 6 секунд

Пример параллельного выполнения того же кода:

(async () => {
    console.time('concurrent');
    await Promise.all([f1(), f2(), f3()]);
    console.timeEnd('concurrent'); 
})();

В два раза быстрее, так как нам потребовалось дождаться только выполнения самой долгой функции f3:

Выполнение параллельных вызовов заняло 3 секунды

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

1 комментарий

Оставить комментарий

*Доступные HTML-теги: a, abbr, blockquote, code, pre, del, i, em, strong, b, strike
*Не будет опубликован