Нарушенные обещания?
Пропуск ошибок, потеря результатов…
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 * 2const quarter = x => x / 4const square = x => x * xconst 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не показывает сразу, кто и что изменил- Сложно отлаживать или логировать из середины вложенных вызовов