Доброго времени суток, друзья!
Представляю вашему вниманию перевод статьи Apal Shah «Common Javascript Promise mistakes every beginner should know and avoid».
Распространенные ошибки при работе с промисами в JavaScript, о которых должен знать каждый
Хотел бы я знать об этих ошибках, когда изучал JavaScript и промисы.
Всякий раз, когда ко мне обращается какой-нибудь разработчик и жалуется на то, что его код не работает или медленно выполняется, я прежде всего обращаю внимание на эти ошибки. Когда я начал программировать 4 года назад, я не знал о них и привык их игнорировать. Однако после назначения в проект, который обрабатывает около миллиона запросов в течение нескольких минут, у меня не было другого выбора, кроме как оптимизировать свой код (поскольку мы достигли уровня, когда дальнейшее вертикальное масштабирование стало невозможным).
Поэтому в данной статье я бы хотел поговорить о самых распространенных ошибках при работе с промисами в JS, на которые многие не обращают внимания.
Ошибка № 1. Использование блока try/catch внутри промиса
Использовать блок try/catch внутри промиса нецелесообразно, поскольку если Ваш код выдаст ошибку (внутри промиса), она будет перехвачена обработчиком ошибок самого промиса.
Речь идет вот о чем:
new Promise((resolve, reject) => {
try {
const data = someFunction()
// ваш код
resolve()
} catch(e) {
reject(e)
}
})
.then(data => console.log(data))
.catch(error => console.log(error))
Вместо этого позвольте коду обработать ошибку вне промиса:
new Promise((resolve, reject) => {
const data = someFunction()
// ваш код
resolve(data)
})
.then(data => console.log(data))
.catch(error => console.log(error))
Это будет работать всегда, за исключением случая, описанного ниже.
Ошибка № 2. Использование асинхронной функции внутри промиса
При использовании асинхронной функции внутри промиса возникают некоторые неприятные побочные эффекты.
Допустим, Вы решили выполнить некоторую асинхронную задачу, добавили в промис ключевое слово «async», и Ваш код выдает ошибку. Однако теперь Вы не можете обработать эту ошибку ни с помощью .catch(), ни с помощью await:
// этот код не сможет перехватить ошибку
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)
}
})();
Каждый раз, когда я встречаю асинхронную функцию внутри промиса, я пытаюсь их разделить. И у меня это получается в 9 из 10 случаев. Тем не менее, это не всегда возможно. В таком случае у Вас нет другого выбора, кроме как использовать блок 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)
}
})();
Ошибка № 3. Забывать про .catch()
Эта одна из тех ошибок, о существовании которой даже не подозреваешь, пока не начнется тестирование. Либо, если Вы какой-нибудь атеист, который не верит в тесты, Ваш код обязательно рухнет в продакшне. Потому что продакшн строго следует закону Мерфи, который гласит: «Anything that can go wrong will go wrong» (можно перевести так: «Если что-то может пойти не так, это обязательно произойдет»; аналогией в русском языке является «закон подлости» — прим. пер.).
Для того, чтобы сделать код элегантнее, можно обернуть промис в try/catch вместо использования .then().catch().
Ошибка № 4. Не использовать Promise.all()
Promise.all() — твой друг.
Если Вы профессиональный разработчик, Вы наверняка понимаете, что я хочу сказать. Если у Вас есть несколько не зависящих друг от друга промисов, Вы можете выполнить их одновременно. По умолчанию, промисы выполняются параллельно, однако если Вам необходимо выполнить их последовательно (с помощью await), это займет много времени. Promise.all() позволяет сильно сократить время ожидания.
const {promisify} = require('util')
const sleep = promisify(setTimeout)
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 секунд
})();
Теперь с Promise.all():
(async() => {
console.time('concurrent')
await Promise.all([f1(), f2(), f3()])
console.timeEnd('concurrent') // около 3 секунд
})();
Ошибка № 5. Неправильное использование Promise.race()
Promise.race() не всегда делает Ваш код быстрее.
Это может показаться странным, но это действительно так. Я не утверждаю, что Promise.race() — бесполезный метод, но Вы должны четко понимать, зачем его используете.
Вы, например, можете использовать Promise.race() для запуска кода после разрешения любого из промисов. Но это не означает, что выполнение кода, следующего за промисами, начнется сразу же после разрешения одного из них. Promise.race() будет ждать разрешения всех промисов и только после этого освободит поток:
const {promisify} = require('util')
const sleep = promisify(setTimeout)
async function f1() {
await sleep(1000)
}
async function f2() {
await sleep(2000)
}
async function f3() {
await sleep(3000)
}
(async() => {
console.time('race')
await Promise.race([f1(), f2(), f3()])
})();
process.on('exit', () => {
console.timeEnd('race') // около 3 секунд, код не стал быстрее!
})
Ошибка № 6. Злоупотребление промисами
Промисы делают код медленнее, так что не злоупотребляйте ими.
Часто приходится видеть разработчиков, использующих длинную цепочку .then(), чтобы их код выглядел лучше. Вы и глазом не успеете моргнуть, как эта цепочка станет слишком длинной. Для того, чтобы наглядно убедиться в негативных последствиях такой ситуации, необходимо (далее я немного отступлю от оригинального текста для того, чтобы описать процесс подробнее, нежели в статье — прим. пер.):
1) создать файл script.js следующего содержания (с лишними промисами):
new Promise((resolve) => {
// некий код, возвращающий данные пользователя
const user = {
name: 'John Doe',
age: 50,
}
resolve(user)
}).then(userObj => {
const {age} = userObj
return age
}).then(age => {
if(age > 25) {
return true
}
throw new Error('Age is less than 25')
}).then(() => {
console.log('Age is greater than 25')
}).catch(e => {
console.log(e.message)
})
2) открыть командную строку (для пользователей Windows: чтобы открыть командную строку в папке с нужным файлом, зажимаем Shift, кликаем правой кнопкой мыши, выбираем «Открыть окно команд»), запустить script.js с помощью следующей команды (должен быть установлен Node.js):
node --trace-events-enabled script.js
3) Node.js создает файл журнала (в моем случае node_trace.1.txt) в папке со скриптом;
4) открываем Chrome (потому что это работает только в нем), вводим в адресной строке «chrome://tracing»;
5) нажимаем Load, загружаем файл журнала, созданного Node.js;
6) открываем вкладку Promise.
Видим примерно следующее:
Зеленые блоки — промисы, выполнение каждого из которых занимает несколько миллисекунд. Следовательно, чем больше будет промисов, тем дольше они будут выполняться.
Перепишем script.js:
new Promise((resolve, reject) => {
const user = {
name: 'John Doe',
age: 50,
}
if(user.age > 25) {
resolve()
} else {
reject('Age is less than 25')
}
}).then(() => {
console.log('Age is greater than 25')
}).catch(e => {
console.log(e.message)
})
Повторим «трассировку».
Видим следующее:
Зеленых блоков (промисов) стало меньше, а значит время выполнения кода сократилось.
Таким образом, использовать несколько промисов следует только в том случае, если Вам необходимо выполнить некоторый асинхронный код.
Благодарю за внимание. Всем хороших выходных! Буду рад любым замечаниям.
vvadzim
А вот вся программа в целом будет ждать завершения всех (ну почти, были вроде исключения) промисов, созданных за время её выполнения. Что и показывает код в примере.
Dartess
Либо автор оригинальной статьи уже исправил этот момент, либо тут очень вольный перевод. aio350 перепроверьте, пожалуйста :)
aio350 Автор
спасибо, поправил
faiwer
Вы что-то не то поправили. Речь идёт о том, что само приложение не умрёт до тех пор пока есть хотя бы 1 событие, которого оно ждёт. Достаточно одного повисшего callback-а, чтобы приложение не умерло само. Но вот выполнение кода следующего за промисами начнётся как раз сразу после "разрешения одного из них"
Там автор пишет:
Не знаю зачем он решил всех запутать словом thread, которое имеет 100500 значений. Но по сути речь идёт о всём контексте в котором запущена js-VM. В случае nodejs это или весь процесс, или worker. В случае браузера это снова или worker или браузерный tab. А сам Promise.prototype.race работает именно так, как от него и ожидают.
adictive_max