Нарушенные обещания?
Пропуск ошибок, потеря результатов…
JavaScript‑промисы сломаны?
В старые времена
Один из самых распространённых мифов о промисах — это якобы недостаточная работа с ошибками.
Много лет назад промисы были действительно плохи в обработке ошибок. Много усилий было вложено в их исправление.
И вот, это было исправлено, даже широко развернуто.
Люди радовались
И,к сожалению, некоторые этого не заметили.
Современные времена
Миф всё ещё жив, я вижу его повсюду: популярные статьи на Medium, на DZone и многие другие источники.
Признаюсь, даже «официальные» ресурсы и документация в основном предлагают хрупкие примеры и плохие привычки. Их часто используют, чтобы «доказать» аргументы против промисов. Некоторые даже советуют «лекарства», которые только ухудшают ситуацию. (примечание: ссылка удалена)
Правила, чтобы не попасть в беду
- Промисам нужно, за что держаться
- Всегда
returnиз ваших функций.
- Всегда
- Используйте реальные экземпляры
Error- Всегда используйте экземпляры
Error.
- Всегда используйте экземпляры
- Обрабатывайте ошибки там, где это имеет смысл
- Всегда вызывайте
.catch(), хотя бы один раз.
- Всегда вызывайте
- Добавляйте ясность именованными функциями 🦄✨
- Предпочитайте именованные функции.
#1 Промисам нужно, за что держаться
Крайне важно, чтобы вы всегда return из своих функций.
Функции‑обратного вызова промиса следуют определённому шаблону в .then(callback) и .catch(callback).
Каждое возвращённое значение передаётся в колбэк следующего .then().
function addTen(number) {
return number + 10;
}
Promise.resolve(10) // 10
.then(addTen) // 20
.then(addTen) // 30
.then(addTen) // 40
.then(console.log) // logs "40"
Плюс от «всегда возвращать»: код становится значительно проще тестировать.
Вопрос: Сколько различных состояний промиса (resolved & rejected) было создано?
Вопрос: Сколько промисов было создано в приведённом выше примере?
#2 Используйте реальные экземпляры Error
JavaScript имеет интересное поведение в отношении ошибок (которое распространяется как на асинхронный, так и на синхронный код).
[см. пример на repl.it:throwing errors in javascript]
Чтобы получить полезные сведения о номере строки и стеке вызовов, необходимо использовать экземпляры Error. Бросать строки не работает так, как в Python или Ruby.
Хотя JavaScript кажется обрабатывает throw "string", вы увидите эту строку в обработчике catch. Однако это будет всё, что вы получите*. Ни один предыдущий фрейм стека не будет включён.
Правильные примеры с new Error:
throw new Error('message') // ✅
Promise.reject(new Error('message')) // ✅
throw Error('message') // ✅
Promise.reject(Error('message')) // ✅
Ниже перечислены типичные антипаттерны:
throw 'error message' // ❌
Promise.reject(-42) // ❌
#3 Обрабатывайте ошибки там, где это имеет смысл
Promise дают удобный способ обработки ошибок через .catch(). По сути это особый вариант .then() — он перехватывает любые ошибки, возникшие в предыдущих .then(). Рассмотрим пример…
Promise.resolve(42)
.then(() => 'hello')
.catch(() => console.log('will not get hit'))
.then(() => throw new Error('totes fail'))
.catch(() => console.log('WILL get hit'))
Хотя .catch() может напоминать обработчик DOM‑события (например, click, keypress), его позиция критична: он может «поймать» только те ошибки, которые возникли выше него.
Переопределять ошибки довольно просто — верните не‑ошибочное значение в колбэке .catch(), и цепочка Promise переключится на выполнение последующих .then()‑колбэков (по сути).
Попробуйте проследить последовательность в следующем примере:
Promise.resolve(42)
.then(() => 'hello')
.then(() => throw new Error('totes fail'))
.catch(() => {
return 99
})
.then(num => num + 1)
.then(console.log) // ожидаемый вывод: 100
Важно понять именно последовательность выполнения.
Хотя пример глупый, он предназначен для демонстрации того, как ошибки и данные проходят сквозь цепочку Promise.
Ниже схема последовательности:
42— начальное значение.hello— всегда возвращается следующим методом.- Мы игнорируем предыдущее значение и бросаем ошибку с сообщением
'totes fail'. .catch()перехватывает ошибку и вместо неё возвращает99, которое будет получено любыми последующими.then().- Инкрементируем
num, получаем100. - Метод
console.logполучает100и выводит его! :tada:
Вопрос: Что произойдёт, если в цепочке подряд идут два .catch()? Может ли второй когда‑нибудь выполниться? Придумайте сценарий применения.
Вопрос: Как .catch() может игнорировать ошибки? Как предотвратить преждевременный выход из Promise.all из‑за ошибок?
#4 Добавьте ясность с именованными функциями 🦄✨
Сравните читаемость двух примеров:
Анонимные: ❌
Promise.resolve(10) // 10
.then(x => x * 2) // 20
.then(x => x / 4) // 5
.then(x => x * x) // 25
.then(x => x.toFixed(2)) // "25.00"
.then(x => console.log(x)) // ожидаемый вывод: "25.00"
Именованные: ✅
Promise.resolve(10) // 10
.then(double) // 20
.then(quarter) // 5
.then(square) // 25
.then(format) // "25.00"
.then(log) // ожидаемый вывод: "25.00"
const double = x => x * 2
const quarter = x => x / 4
const square = x => x * x
const format = x => x.toFixed(2)
const log = x => console.log(x)
БОНУС: ✅
Совместимо с методами массива!!!
Именованные функции можно переиспользовать с нашими друзьями из Array.prototype. — включая .map(), .filter(), .every(), .some(), .find()!
Коллекционные конвейеры #FTW:
// IT'S LIKE THE SAME THING :mindblown:
[10, 20] // [ 10, 20 ]
.map(double) // [ 20, 40 ]
.map(quarter) // [ 5, 10 ]
.map(square) // [ 25, 100 ]
.map(format) // [ "25.00", "100.00" ]
.map(log) // expected 2 lines of output: "25.00", "100.00"
А если вам не нравится линейный стиль… У вас всё равно есть простые функции!
Их можно применять в любой комбинации:
// Nesting patern
// ❌ please don't do this, however
const result = format(square(quarter(double(10))))
log(result)
// expected output: "25.00"
Почему вложение функций считается антипаттерном?
- Трудно читаемо для большинства разработчиков
git diffне показывает сразу, кто и что изменил- Сложно отлаживать или логировать из середины вложенных вызовов