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 секунд!

Пример параллельного выполнения того же кода:
(async () => {
console.time('concurrent');
await Promise.all([f1(), f2(), f3()]);
console.timeEnd('concurrent');
})();
В два раза быстрее, так как нам потребовалось дождаться только выполнения самой долгой функции f3:

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