Промис не дожидается ответа из асинхронной функции [дубликат]
Я новичок в JS и еще учусь. Решил попробовать написать расширение для Chrome. Расширение принимает список из ключевых слов и после этого цикл должен формировать из каждого слова ссылку и создавать новую вкладку, затем на этой вкладке он отбирает некоторые эллементы (ссылки) и добавляет их в переменную, далее вкладка закрывается, а потом функция возвращает массив со всеми ссылками для всех ключевых слов.
const linkedInSearchElement = (keyWords) => < return new Promise((resolve, reject) =>< chrome.tabs.create(< url: `https://www.linkedin.com/sales/search/company?keywords=$&page=1&searchSessionId=0ehP5LShQGivgqjzo9LqjQ%3D%3D` >, (tab) => < resolve(tab.id); >); >).then((tabId) => < return new Promise((resolve, reject) => < chrome.tabs.executeScript(tabId, < 'code': 'function linksWineSearch ()console.log(wineLinks); return wineLinks; > linksWineSearch(); ' >, (wineLinks) => < resolve([wineLinks, tabId]); >); >) >).then((linksAndId) => < return new Promise((resolve, reject) => < chrome.tabs.remove(linksAndId[1], () =>< resolve(linksAndId[0]); >); >) >).catch(error => console.log(error)); >; const linkedInGeneralSearch = (readyNames) => < const compNames = [. readyNames]; let linkList = []; for (let i = 0; i < 2; i++) < const keyWords = compNames[i]; console.log(linkedInSearchElement(keyWords)); const searchElementResult = linkedInSearchElement(keyWords); linkList = [. searchElementResult]; >return linkList; >
Но почему-то после ‘chrome.tabs.create’ все ‘.then’ пропускаются и функция ‘linkedInSearchElement’ выполняется заново и так далее. И только после всех итераций по очереди начинают выполнятся все ‘.then’. Почему все .then не выполняются по порядку, функция запускается заново до окончания предыдущей? P.S. Я старался изучить асинхронное программирование в JS, но тут не могу разобраться)) Заранее спасибо за ответы!
Почему VS Code не подчёркивает ошибки в js файлах?
Добрый день. Решил с PHP попробовать перейти на Node JS и сразу же у меня встала проблема:
VS Code не подчёркивает красным опечатки/ошибки/несуществующие переменные и функции.
Какие у вас есть решения? Если с VS Code не получится, то что посоветуете использовать вместо его?
- Вопрос задан более двух лет назад
- 970 просмотров
16 комментариев
Простой 16 комментариев

Так может стоит поставить линтер? А так же спелл чекер.
ВС код ─ это текстовый редактор с тонной плагинов, сам он мало что умеет делать.
haskell_md2 @haskell_md2 Автор вопроса
WbICHA, я поставил ESLint, следуя по инструкции, но он почему-то не работает.

WbICHA, всё таки текстовый редактор — про другое.

haskell_md2, так ево и настраивать нужно для каждого проекта вроде как. типа в рабочей папке npm init и все такое потом создать конф файло для eslint, я конечно не спец по javascrit’овским делам но с первого раза разобрался. Ничего сложного. Там везде подсказки. PS: Забыл скрин вставить. Как-то так, все подсвечивает, прям как в IDE.
screenshot


Да это ж равиоли. — Ну это же пельмени.
Да, но все-таки равиоли.
Но вначале они — пельмени, а уж потом все остальное.
Это ж пельмени, Михалыч.
Нда, равиоли.
(с) Особенности национальной рыбалки

Sergei Nazarenko, да просто.
текст можно писать и в ide, делает ли это ее текстовым редактором?
а тетрадь?
все таки есть давно устоявшаяся терминология, которая отличает текстовый редактор от редактора кода.

DevMan, почему же? Вскод без плагинов даже синтаксис не подсвечивает. По моему.
В целом, конечно, да, в нём есть ряд фич, так что редактор кода будет правильнее, но разве это не синоним в рамках этой темы?
делает ли это ее текстовым редактором?
Вообще, если позанудствовать, то да, делает.) Текстовый редактор ─ это часть иде.

DevMan, Ну скажем так любая IDE есть текстовый редактор но не каждый текстовый редактор — есть IDE. Даже тетрадь или там Notepad.exe или TextEdit.app и тд. может быть редактором кода — почему нет, если она справляется со своей задачей, написанием и редактированием кода. Тут вопрос только в удобстве. Ну а что по поводу IDE ну она по факту тоже есть редактор просто с кучей интегрированных функций, там компиляторы отладчики и тд. Ну как тетрадь есть просто тетрадь а есть органайзер. И по сути органайзер он тоже тетрадь просто более продвинутый там всякие фишки плюшки в виде закладок календарей и тд. PS: И все же они пельмени тысызыть.

WbICHA, если позанудствовать, то код можно писать и в ворде, и в виндовом нотепаде, а текст в идее.
но, повторюсь, есть давно устоявшаяся терминология и деление по функционалу.
а vsc из коробки таки подсвечивает синтаксис. как минимум популярных языков.

Sergei Nazarenko, потому что редкатор кода – это не тупо набор кода, а и соответствующий функционал: подсветка кода, дополнение кода, хэлп и все такое.

DevMan, Да все так никто не спорит. Но все же они пельмени (с)

DevMan, Вот есть четкое разделение грузовой и легковой автомобиль и с этим не посвпоришь. На легковом авто тоже можно песок возить ну не так много конечно. Но в итоге то и тот и тот автомобиль

Sergei Nazarenko, a любой редактор – это редактор. даже графический.
всегда ваш, КО!

DevMan, Ну да, почему нет если редактирует — значит редактор, даже корректор в тетради, делает из тетради редактор ))

Sergei Nazarenko, именно.
но есть градации:
– текстовый редактор
– редактор кода
– графический редактор
– редактор печатных плат
– редактор электрических схем
etc
я указал, что vsc принадлежит отличной от указанной в коменте категории. так с чем конкретно вы не согласны/спорите?
Promise
Объект Promise используется для отложенных и асинхронных вычислений.
Интерактивный пример
Синтаксис
new Promise(executor); new Promise(function(resolve, reject) . >);
Параметры
Объект функции с двумя аргументами resolve и reject . Функция executor получает оба аргумента и выполняется сразу, ещё до того как конструктор вернёт созданный объект. Первый аргумент ( resolve ) вызывает успешное исполнение промиса, второй ( reject ) отклоняет его. Обычно функция executor описывает выполнение какой-то асинхронной работы, по завершении которой необходимо вызвать функцию resolve или reject . Обратите внимание, что возвращаемое значение функции executor игнорируется.
Описание
Интерфейс Promise (промис) представляет собой обёртку для значения, неизвестного на момент создания промиса. Он позволяет обрабатывать результаты асинхронных операций так, как если бы они были синхронными: вместо конечного результата асинхронного метода возвращается своего рода обещание (дословный перевод слова «промис») получить результат в некоторый момент в будущем.
Promise может находиться в трёх состояниях:
- ожидание (pending): начальное состояние, не исполнен и не отклонён.
- исполнено (fulfilled): операция завершена успешно.
- отклонено (rejected): операция завершена с ошибкой.
При создании промис находится в ожидании (pending), а затем может стать исполненным (fulfilled), вернув полученный результат (значение), или отклонённым (rejected), вернув причину отказа. В любом из этих случаев вызывается обработчик, прикреплённый к промису методом then . (Если в момент назначения обработчика промис уже исполнен или отклонён, обработчик всё равно будет вызван, т.е. асинхронное исполнение промиса и назначение обработчика не будет происходить в «состоянии гонки», как, например, в случае с событиями в DOM.)
Так как методы Promise.prototype.then() и Promise.prototype.catch() сами возвращают промис, их можно вызывать цепочкой, создавая соединения.

Примечание: говорят, что промис находится в состоянии завершён (settled) когда он или исполнен или отклонён, т.е. в любом состоянии, кроме ожидания (это лишь форма речи, не являющаяся настоящим состоянием промиса). Также можно встретить термин исполнен (resolved) — это значит что промис завершён или «заблокирован» в ожидании завершения другого промиса. В статье состояния и fates приводится более подробное описание терминологии.
Свойства
Значение свойства всегда равно 1 (количество аргументов конструктора).
Представляет прототип для конструктора Promise .
Методы
Ожидает исполнения всех промисов или отклонения любого из них.
Возвращает промис, который исполнится после исполнения всех промисов в iterable . В случае, если любой из промисов будет отклонён, Promise.all будет также отклонён.
Ожидает завершения всех полученных промисов (как исполнения так и отклонения).
Возвращает промис, который исполняется когда все полученные промисы завершены (исполнены или отклонены), содержащий массив результатов исполнения полученных промисов.
Ожидает исполнения или отклонения любого из полученных промисов.
Возвращает промис, который будет исполнен или отклонён с результатом исполнения первого исполненного или отклонённого промиса из iterable .
Возвращает промис, отклонённый из-за reason .
Возвращает промис, исполненный с результатом value .
Создание промиса
Объект Promise создаётся при помощи ключевого слова new и своего конструктора. Конструктор Promise принимает в качестве аргумента функцию, называемую «исполнитель» (executor function). Эта функция должна принимать две функции-колбэка в качестве параметров. Первый из них ( resolve ) вызывается, когда асинхронная операция завершилась успешно и вернула результат своего исполнения в виде значения. Второй колбэк ( reject ) вызывается, когда операция не удалась, и возвращает значение, указывающее на причину неудачи, чаще всего объект ошибки.
const myFirstPromise = new Promise((resolve, reject) => // выполняется асинхронная операция, которая в итоге вызовет: // // resolve(someValue); // успешное завершение // или // reject("failure reason"); // неудача >);
Чтобы снабдить функцию функциональностью промисов, нужно просто вернуть в ней объект Promise :
function myAsyncFunction(url) return new Promise((resolve, reject) => const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.onload = () => resolve(xhr.responseText); xhr.onerror = () => reject(xhr.statusText); xhr.send(); >); >
Примеры
Простой пример
jslet myFirstPromise = new Promise((resolve, reject) => // Мы вызываем resolve(. ), когда асинхронная операция завершилась успешно, и reject(. ), когда она не удалась. // В этом примере мы используем setTimeout(. ), чтобы симулировать асинхронный код. // В реальности вы, скорее всего, будете использовать XHR, HTML5 API или что-то подобное. setTimeout(function () resolve("Success!"); // Ура! Всё прошло хорошо! >, 250); >); myFirstPromise.then((successMessage) => // successMessage - это что угодно, что мы передали в функцию resolve(. ) выше. // Это необязательно строка, но если это всего лишь сообщение об успешном завершении, это наверняка будет она. console.log("Ура! " + successMessage); >);Продвинутый пример
htmlbutton id="btn">Создать Promise!button> div id="log">div>исполнение промиса протоколируется при помощи продолжения p1.then . Это показывает как синхронная часть метода отвязана от асинхронного завершения промиса.
var promiseCount = 0; function testPromise() var thisPromiseCount = ++promiseCount; var log = document.getElementById('log'); log.insertAdjacentHTML('beforeend', thisPromiseCount + ') Запуск (запуск синхронного кода) '); // Создаём промис, возвращающее 'result' (по истечении 3-х секунд) var p1 = new Promise( // Функция разрешения позволяет завершить успешно или // отклонить промис function(resolve, reject) log.insertAdjacentHTML('beforeend', thisPromiseCount + ') Запуск промиса (запуск асинхронного кода) '); // Это всего лишь пример асинхронности window.setTimeout( function() // Промис исполнен! resolve(thisPromiseCount) >, Math.random() * 2000 + 1000); >); // Указываем, что сделать с исполненным промисом p1.then( // Записываем в протокол function(val) log.insertAdjacentHTML('beforeend', val + ') Промис исполнен (асинхронный код завершён) '); >); log.insertAdjacentHTML('beforeend', thisPromiseCount + ') Промис создан (синхронный код завершён) '); >if ("Promise" in window) btn = document.getElementById("btn"); btn.addEventListener("click", testPromise); > else log = document.getElementById("log"); log.innerHTML = "Live example not available as your browser doesn't support the Promise interface."; >if ("Promise" in window) let btn = document.getElementById("btn"); btn.addEventListener("click", testPromise); > else log = document.getElementById("log"); log.innerHTML = "Демонстрация невозможна, поскольку ваш браузер не поддерживает интерфейсPromise; >."Данный пример запускается при нажатии на кнопку. Для этого вам необходим браузер, поддерживающий Promise . При последовательных нажатиях на кнопку с коротким интервалом, вы можете увидеть как различные промиса будут исполнены один за другим.
Загрузка изображения при помощи XHR
Другой простой пример использования Promise и XMLHttpRequest для загрузки изображения доступен в репозитории MDNpromise-test на GitHub. Вы также можете посмотреть его в действии. Каждый шаг прокомментирован и вы можете подробно исследовать Promise и XHR.
Спецификации
| Specification |
|---|
| ECMAScript Language Specification # sec-promise-objects |
Совместимость с браузерами
BCD tables only load in the browser
Смотрите также
- Спецификация Promises/A+
- Jake Archibald: JavaScript Promises: There and Back Again
- Domenic Denicola: Callbacks, Promises, and Coroutines – Asynchronous Programming Pattern in JavaScript
- Matt Greer: JavaScript Promises . In Wicked Detail
- Forbes Lindesay: promisejs.org
- Nolan Lawson: We have a problem with promises — Common mistakes with promises
- Promise polyfill
- Udacity: JavaScript Promises
Found a content problem with this page?
- Edit the page on GitHub.
- Report the content issue.
- View the source on GitHub.
This page was last modified on 7 авг. 2023 г. by MDN contributors.
Your blueprint for a better internet.
Promise
Материал на этой странице устарел, поэтому скрыт из оглавления сайта.
Более новая информация по этой теме находится на странице https://learn.javascript.ru/promise-basics.
Promise (обычно их так и называют «промисы») – предоставляют удобный способ организации асинхронного кода.
В современном JavaScript промисы часто используются в том числе и неявно, при помощи генераторов, но об этом чуть позже.
Что такое Promise?
Promise – это специальный объект, который содержит своё состояние. Вначале pending («ожидание»), затем – одно из: fulfilled («выполнено успешно») или rejected («выполнено с ошибкой»).

На promise можно навешивать колбэки двух типов:
- onFulfilled – срабатывают, когда promise в состоянии «выполнен успешно».
- onRejected – срабатывают, когда promise в состоянии «выполнен с ошибкой».
Способ использования, в общих чертах, такой:
- Код, которому надо сделать что-то асинхронно, создаёт объект promise и возвращает его.
- Внешний код, получив promise , навешивает на него обработчики.
- По завершении процесса асинхронный код переводит promise в состояние fulfilled (с результатом) или rejected (с ошибкой). При этом автоматически вызываются соответствующие обработчики во внешнем коде.
Синтаксис создания Promise :
var promise = new Promise(function(resolve, reject) < // Эта функция будет вызвана автоматически // В ней можно делать любые асинхронные операции, // А когда они завершатся — нужно вызвать одно из: // resolve(результат) при успешном выполнении // reject(ошибка) при ошибке >)
Универсальный метод для навешивания обработчиков:
promise.then(onFulfilled, onRejected)
- onFulfilled – функция, которая будет вызвана с результатом при resolve .
- onRejected – функция, которая будет вызвана с ошибкой при reject .
С его помощью можно назначить как оба обработчика сразу, так и только один:
// onFulfilled сработает при успешном выполнении promise.then(onFulfilled) // onRejected сработает при ошибке promise.then(null, onRejected)
Для того, чтобы поставить обработчик только на ошибку, вместо .then(null, onRejected) можно написать .catch(onRejected) – это то же самое.
Синхронный throw – то же самое, что reject
Если в функции промиса происходит синхронный throw (или иная ошибка), то вызывается reject :
'use strict'; let p = new Promise((resolve, reject) => < // то же что reject(new Error("o_O")) throw new Error("o_O"); >) p.catch(alert); // Error: o_O
Посмотрим, как это выглядит вместе, на простом примере.
Пример с setTimeout
Возьмём setTimeout в качестве асинхронной операции, которая должна через некоторое время успешно завершиться с результатом «result»:
'use strict'; // Создаётся объект promise let promise = new Promise((resolve, reject) => < setTimeout(() =>< // переведёт промис в состояние fulfilled с результатом "result" resolve("result"); >, 1000); >); // promise.then навешивает обработчики на успешный результат или ошибку promise .then( result => < // первая функция-обработчик - запустится при вызове resolve alert("Fulfilled: " + result); // result - аргумент resolve >, error => < // вторая функция - запустится при вызове reject alert("Rejected: " + error); // error - аргумент reject >);
В результате запуска кода выше – через 1 секунду выведется «Fulfilled: result».
А если бы вместо resolve("result") был вызов reject("error") , то вывелось бы «Rejected: error». Впрочем, как правило, если при выполнении возникла проблема, то reject вызывают не со строкой, а с объектом ошибки типа new Error :
// Этот promise завершится с ошибкой через 1 секунду var promise = new Promise((resolve, reject) => < setTimeout(() =>< reject(new Error("время вышло!")); >, 1000); >); promise .then( result => alert("Fulfilled: " + result), error => alert("Rejected: " + error.message) // Rejected: время вышло! );
Конечно, вместо setTimeout внутри функции промиса может быть и запрос к серверу и ожидание ввода пользователя, или другой асинхронный процесс. Главное, чтобы по своему завершению он вызвал resolve или reject , которые передадут результат обработчикам.
Только один аргумент
Функции resolve/reject принимают ровно один аргумент – результат/ошибку.
Именно он передаётся обработчикам в .then , как можно видеть в примерах выше.
Promise после reject/resolve – неизменны
Заметим, что после вызова resolve/reject промис уже не может «передумать».
Когда промис переходит в состояние «выполнен» – с результатом (resolve) или ошибкой (reject) – это навсегда.
'use strict'; let promise = new Promise((resolve, reject) => < // через 1 секунду готов результат: result setTimeout(() =>resolve("result"), 1000); // через 2 секунды — reject с ошибкой, он будет проигнорирован setTimeout(() => reject(new Error("ignored")), 2000); >); promise .then( result => alert("Fulfilled: " + result), // сработает error => alert("Rejected: " + error) // не сработает );
В результате вызова этого кода сработает только первый обработчик then , так как после вызова resolve промис уже получил состояние (с результатом), и в дальнейшем его уже ничто не изменит.
Последующие вызовы resolve/reject будут просто проигнорированы.
А так – наоборот, ошибка будет раньше:
'use strict'; let promise = new Promise((resolve, reject) => < // reject вызван раньше, resolve будет проигнорирован setTimeout(() =>reject(new Error("error")), 1000); setTimeout(() => resolve("ignored"), 2000); >); promise .then( result => alert("Fulfilled: " + result), // не сработает error => alert("Rejected: " + error) // сработает );
Промисификация
Промисификация – это когда берут асинхронную функциональность и делают для неё обёртку, возвращающую промис.
После промисификации использование функциональности зачастую становится гораздо удобнее.
В качестве примера сделаем такую обёртку для запросов при помощи XMLHttpRequest.
Функция httpGet(url) будет возвращать промис, который при успешной загрузке данных с url будет переходить в fulfilled с этими данными, а при ошибке – в rejected с информацией об ошибке:
function httpGet(url) < return new Promise(function(resolve, reject) < var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.onload = function() < if (this.status == 200) < resolve(this.response); >else < var error = new Error(this.statusText); error.code = this.status; reject(error); >>; xhr.onerror = function() < reject(new Error("Network Error")); >; xhr.send(); >); >
Как видно, внутри функции объект XMLHttpRequest создаётся и отсылается как обычно, при onload/onerror вызываются, соответственно, resolve (при статусе 200) или reject .
httpGet("/article/promise/user.json") .then( response => alert(`Fulfilled: $`), error => alert(`Rejected: $`) );
Метод fetch
Заметим, что ряд современных браузеров уже поддерживает fetch – новый встроенный метод для AJAX-запросов, призванный заменить XMLHttpRequest. Он гораздо мощнее, чем httpGet . И – да, этот метод использует промисы. Полифил для него доступен на https://github.com/github/fetch.
Цепочки промисов
«Чейнинг» (chaining), то есть возможность строить асинхронные цепочки из промисов – пожалуй, основная причина, из-за которой существуют и активно используются промисы.
Например, мы хотим по очереди:
- Загрузить данные посетителя с сервера (асинхронно).
- Затем отправить запрос о нём на github (асинхронно).
- Когда это будет готово, вывести его github-аватар на экран (асинхронно).
- …И сделать код расширяемым, чтобы цепочку можно было легко продолжить.
Вот код для этого, использующий функцию httpGet , описанную выше:
'use strict'; // сделать запрос httpGet('/article/promise/user.json') // 1. Получить данные о пользователе в JSON и передать дальше .then(response => < console.log(response); let user = JSON.parse(response); return user; >) // 2. Получить информацию с github .then(user => < console.log(user); return httpGet(`https://api.github.com/users/$`); >) // 3. Вывести аватар на 3 секунды (можно с анимацией) .then(githubUser => < console.log(githubUser); githubUser = JSON.parse(githubUser); let img = new Image(); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.appendChild(img); setTimeout(() =>img.remove(), 3000); // (*) >);
Самое главное в этом коде – последовательность вызовов:
httpGet(. ) .then(. ) .then(. ) .then(. )
При чейнинге, то есть последовательных вызовах .then…then…then , в каждый следующий then переходит результат от предыдущего. Вызовы console.log оставлены, чтобы при запуске можно было посмотреть конкретные значения, хотя они здесь и не очень важны.
Если очередной then вернул промис, то далее по цепочке будет передан не сам этот промис, а его результат.
- Функция в первом then возвращает «обычное» значение user . Это значит, что then возвратит промис в состоянии «выполнен» с user в качестве результата. Он станет аргументом в следующем then .
- Функция во втором then возвращает промис (результат нового вызова httpGet ). Когда он будет завершён (может пройти какое-то время), то будет вызван следующий then с его результатом.
- Третий then ничего не возвращает.
Схематично его работу можно изобразить так:

Значком «песочные часы» помечены периоды ожидания, которых всего два: в исходном httpGet и в подвызове далее по цепочке.
Если then возвращает промис, то до его выполнения может пройти некоторое время, оставшаяся часть цепочки будет ждать.
То есть, логика довольно проста:
- В каждом then мы получаем текущий результат работы.
- Можно его обработать синхронно и вернуть результат (например, применить JSON.parse ). Или же, если нужна асинхронная обработка – инициировать её и вернуть промис.
Обратим внимание, что последний then в нашем примере ничего не возвращает. Если мы хотим, чтобы после setTimeout (*) асинхронная цепочка могла быть продолжена, то последний then тоже должен вернуть промис. Это общее правило: если внутри then стартует новый асинхронный процесс, то для того, чтобы оставшаяся часть цепочки выполнилась после его окончания, мы должны вернуть промис.
В данном случае промис должен перейти в состояние «выполнен» после срабатывания setTimeout .
Строку (*) для этого нужно переписать так:
.then(githubUser => < . // вместо setTimeout(() =>img.remove(), 3000); (*) return new Promise((resolve, reject) => < setTimeout(() =>< img.remove(); // после таймаута — вызов resolve, // можно без результата, чтобы управление перешло в следующий then // (или можно передать данные пользователя дальше по цепочке) resolve(); >, 3000); >); >)
Теперь, если к цепочке добавить ещё then , то он будет вызван после окончания setTimeout .
Перехват ошибок
Выше мы рассмотрели «идеальный случай» выполнения, когда ошибок нет.
А что, если github не отвечает? Или JSON.parse бросил синтаксическую ошибку при обработке данных?
Да мало ли, где ошибка…
Правило здесь очень простое.
При возникновении ошибки – она отправляется в ближайший обработчик onRejected .
Такой обработчик нужно поставить через второй аргумент .then(. onRejected) или, что то же самое, через .catch(onRejected) .
Чтобы поймать всевозможные ошибки, которые возникнут при загрузке и обработке данных, добавим catch в конец нашей цепочки:
'use strict'; // в httpGet обратимся к несуществующей странице httpGet('/page-not-exists') .then(response => JSON.parse(response)) .then(user => httpGet(`https://api.github.com/users/$`)) .then(githubUser => < githubUser = JSON.parse(githubUser); let img = new Image(); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.appendChild(img); return new Promise((resolve, reject) => < setTimeout(() =>< img.remove(); resolve(); >, 3000); >); >) .catch(error => < alert(error); // Error: Not Found >);
В примере выше ошибка возникает в первом же httpGet , но catch с тем же успехом поймал бы ошибку во втором httpGet или в JSON.parse .
Принцип очень похож на обычный try..catch : мы делаем асинхронную цепочку из .then , а затем, в том месте кода, где нужно перехватить ошибки, вызываем .catch(onRejected) .
А что после catch ?
Обработчик .catch(onRejected) получает ошибку и должен обработать её.
Есть два варианта развития событий:
- Если ошибка не критичная, то onRejected возвращает значение через return , и управление переходит в ближайший .then(onFulfilled) .
- Если продолжить выполнение с такой ошибкой нельзя, то он делает throw , и тогда ошибка переходит в следующий ближайший .catch(onRejected) .
Это также похоже на обычный try..catch – в блоке catch ошибка либо обрабатывается, и тогда выполнение кода продолжается как обычно, либо он делает throw . Существенное отличие – в том, что промисы асинхронные, поэтому при отсутствии внешнего .catch ошибка не «вываливается» в консоль и не «убивает» скрипт.
Ведь возможно, что новый обработчик .catch будет добавлен в цепочку позже.
Промисы в деталях
Самым основным источником информации по промисам является, разумеется, стандарт.
Чтобы наше понимание промисов было полным, и мы могли с лёгкостью разрешать сложные ситуации, посмотрим внимательнее, что такое промис и как он работает, но уже не в общих словах, а детально, в соответствии со стандартом ECMAScript.
Согласно стандарту, у объекта new Promise(executor) при создании есть четыре внутренних свойства:

- PromiseState – состояние, вначале «pending».
- PromiseResult – результат, при создании значения нет.
- PromiseFulfillReactions – список функций-обработчиков успешного выполнения.
- PromiseRejectReactions – список функций-обработчиков ошибки.
Когда функция-executor вызывает reject или resolve , то PromiseState становится "resolved" или "rejected" , а все функции-обработчики из соответствующего списка перемещаются в специальную системную очередь "PromiseJobs" .
Эта очередь автоматически выполняется, когда интерпретатору «нечего делать». Иначе говоря, все функции-обработчики выполнятся асинхронно, одна за другой, по завершении текущего кода, примерно как setTimeout(. 0) .
Исключение из этого правила – если resolve возвращает другой Promise . Тогда дальнейшее выполнение ожидает его результата (в очередь помещается специальная задача), и функции-обработчики выполняются уже с ним.
Добавляет обработчики в списки один метод: .then(onResolved, onRejected) . Метод .catch(onRejected) – всего лишь сокращённая запись .then(null, onRejected) .
Он делает следующее:
- Если PromiseState == "pending" , то есть промис ещё не выполнен, то обработчики добавляются в соответствующие списки.
- Иначе обработчики сразу помещаются в очередь на выполнение.
Здесь важно, что обработчики можно добавлять в любой момент. Можно до выполнения промиса (они подождут), а можно – после (выполнятся в ближайшее время, через асинхронную очередь).
// Промис выполнится сразу же var promise = new Promise((resolve, reject) => resolve(1)); // PromiseState = "resolved" // PromiseResult = 1 // Добавили обработчик к выполненному промису promise.then(alert); // . он сработает тут же
Разумеется, можно добавлять и много обработчиков на один и тот же промис:
// Промис выполнится сразу же var promise = new Promise((resolve, reject) => resolve(1)); promise.then( function f1(result) < alert(result); // 1 return 'f1'; >) promise.then( function f2(result) < alert(result); // 1 return 'f2'; >)
Вид объекта promise после этого:

На этой иллюстрации можно увидеть добавленные нами обработчики f1 , f2 , а также – автоматически добавленные обработчики ошибок "Thrower" .
Дело в том, что .then , если один из обработчиков не указан, добавляет его «от себя», следующим образом:
- Для успешного выполнения – функция Identity , которая выглядит как arg => arg , то есть возвращает аргумент без изменений.
- Для ошибки – функция Thrower , которая выглядит как arg => throw arg , то есть генерирует ошибку.
Это, по сути дела, формальность, но без неё некоторые особенности поведения промисов могут «не сойтись» в общую логику, поэтому мы упоминаем о ней здесь.
Обратим внимание, в этом примере намеренно не используется чейнинг. То есть, обработчики добавляются именно на один и тот же промис.
Поэтому оба alert выдадут одно значение 1 .
Все функции из списка обработчиков вызываются с результатом промиса, одна за другой. Никакой передачи результатов между обработчиками в рамках одного промиса нет, а сам результат промиса ( PromiseResult ) после установки не меняется.
Поэтому, чтобы продолжить работу с результатом, используется чейнинг.
Для того, чтобы результат обработчика передать следующей функции, .then создаёт новый промис и возвращает его.
В примере выше создаётся два таких промиса (т.к. два вызова .then ), каждый из которых даёт свою ветку выполнения:

Изначально эти новые промисы – «пустые», они ждут. Когда в будущем выполнятся обработчики f1, f2 , то их результат будет передан в новые промисы по стандартному принципу:

- Если вернётся обычное значение (не промис), новый промис перейдёт в "resolved" с ним.
- Если был throw , то новый промис перейдёт в состояние "rejected" с ошибкой.
- Если вернётся промис, то используем его результат (он может быть как resolved , так и rejected ).
Дальше выполнятся уже обработчики на новом промисе, и так далее.
Чтобы лучше понять происходящее, посмотрим на цепочку, которая получается в процессе написания кода для показа github-аватара.
Первый промис и обработка его результата:
httpGet('/article/promise/user.json') .then(JSON.parse)
Если промис завершился через resolve , то результат – в JSON.parse , если reject – то в Thrower.
Как было сказано выше, Thrower – это стандартная внутренняя функция, которая автоматически используется, если второй обработчик не указан.
Можно считать, что второй обработчик выглядит так:
httpGet('/article/promise/user.json') .then(JSON.parse, err => throw err)
Заметим, что когда обработчик в промисах делает throw – в данном случае, при ошибке запроса, то такая ошибка не «валит» скрипт и не выводится в консоли. Она просто будет передана в ближайший следующий обработчик onRejected .
Добавим в код ещё строку:
httpGet('/article/promise/user.json') .then(JSON.parse) .then(user => httpGet(`https://api.github.com/users/$`))
Цепочка «выросла вниз»:
Функция JSON.parse либо возвращает объект с данными, либо генерирует ошибку (что расценивается как reject ).
Если всё хорошо, то then(user => httpGet(…)) вернёт новый промис, на который стоят уже два обработчика:
httpGet('/article/promise/user.json') .then(JSON.parse) .then(user => httpGet(`https://api.github.com/users/$`)) .then( JSON.parse, function avatarError(error) < if (error.code == 404) < return ; > else < throw error; >> >)
Наконец-то хоть какая-то обработка ошибок!
Обработчик avatarError перехватит ошибки, которые были ранее. Функция httpGet при генерации ошибки записывает её HTTP-код в свойство error.code , так что мы легко можем понять – что это:
- Если страница на Github не найдена – можно продолжить выполнение, используя «аватар по умолчанию»
- Иначе – пробрасываем ошибку дальше.
Итого, после добавления оставшейся части цепочки, картина получается следующей:
'use strict'; httpGet('/article/promise/userNoGithub.json') .then(JSON.parse) .then(user => httpGet(`https://api.github.com/users/$`)) .then( JSON.parse, function githubError(error) < if (error.code == 404) < return ; > else < throw error; >> ) .then(function showAvatar(githubUser) < let img = new Image(); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.appendChild(img); setTimeout(() =>img.remove(), 3000); >) .catch(function genericError(error) < alert(error); // Error: Not Found >);
В конце срабатывает общий обработчик genericError , который перехватывает любые ошибки. В данном случае ошибки, которые в него попадут, уже носят критический характер, что-то серьёзно не так. Чтобы посетитель не удивился отсутствию информации, мы показываем ему сообщение об этом.
Можно и как-то иначе вывести уведомление о проблеме, главное – не забыть обработать ошибки в конце. Если последнего catch не будет, а цепочка завершится с ошибкой, то посетитель об этом не узнает.
В консоли тоже ничего не будет, так как ошибка остаётся «внутри» промиса, ожидая добавления следующего обработчика onRejected , которому будет передана.
Итак, мы рассмотрели основные приёмы использования промисов. Далее – посмотрим некоторые полезные вспомогательные методы.
Параллельное выполнение
Что, если мы хотим осуществить несколько асинхронных процессов одновременно и обработать их результат?
В классе Promise есть следующие статические методы.
Promise.all(iterable)
Вызов Promise.all(iterable) получает массив (или другой итерируемый объект) промисов и возвращает промис, который ждёт, пока все переданные промисы завершатся, и переходит в состояние «выполнено» с массивом их результатов.
Promise.all([ httpGet('/article/promise/user.json'), httpGet('/article/promise/guest.json') ]).then(results => < alert(results); >);
Допустим, у нас есть массив с URL.
let urls = [ '/article/promise/user.json', '/article/promise/guest.json' ];
Чтобы загрузить их параллельно, нужно:
- Создать для каждого URL соответствующий промис.
- Обернуть массив таких промисов в Promise.all .
'use strict'; let urls = [ '/article/promise/user.json', '/article/promise/guest.json' ]; Promise.all( urls.map(httpGet) ) .then(results => < alert(results); >);
Заметим, что если какой-то из промисов завершился с ошибкой, то результатом Promise.all будет эта ошибка. При этом остальные промисы игнорируются.
Promise.all([ httpGet('/article/promise/user.json'), httpGet('/article/promise/guest.json'), httpGet('/article/promise/no-such-page.json') // (нет такой страницы) ]).then( result => alert("не сработает"), error => alert("Ошибка: " + error.message) // Ошибка: Not Found )
Promise.race(iterable)
Вызов Promise.race , как и Promise.all , получает итерируемый объект с промисами, которые нужно выполнить, и возвращает новый промис.
Но, в отличие от Promise.all , результатом будет только первый успешно выполнившийся промис из списка. Остальные игнорируются.
Promise.race([ httpGet('/article/promise/user.json'), httpGet('/article/promise/guest.json') ]).then(firstResult => < firstResult = JSON.parse(firstResult); alert( firstResult.name ); // iliakan или guest, смотря что загрузится раньше >);
Promise.resolve(value)
Вызов Promise.resolve(value) создаёт успешно выполнившийся промис с результатом value .
Он аналогичен конструкции:
new Promise((resolve) => resolve(value))
Promise.resolve используют, когда хотят построить асинхронную цепочку, и начальный результат уже есть.
Promise.resolve(window.location) // начать с этого значения .then(httpGet) // вызвать для него httpGet .then(alert) // и вывести результат
Promise.reject(error)
Аналогично Promise.reject(error) создаёт уже выполнившийся промис, но не с успешным результатом, а с ошибкой error .
Promise.reject(new Error(". ")) .catch(alert) // Error: .
Метод Promise.reject используется очень редко, гораздо реже чем resolve , потому что ошибка возникает обычно не в начале цепочки, а в процессе её выполнения.
Итого
- Промис – это специальный объект, который хранит своё состояние, текущий результат (если есть) и колбэки.
- При создании new Promise((resolve, reject) => . ) автоматически запускается функция-аргумент, которая должна вызвать resolve(result) при успешном выполнении и reject(error) – при ошибке.
- Аргумент resolve/reject (только первый, остальные игнорируются) передаётся обработчикам на этом промисе.
- Обработчики назначаются вызовом .then/catch .
- Для передачи результата от одного обработчика к другому используется чейнинг.
У промисов есть некоторые ограничения. В частности, стандарт не предусматривает какой-то метод для «отмены» промиса, хотя в ряде ситуаций (http-запросы) это было бы довольно удобно. Возможно, он появится в следующей версии стандарта JavaScript.
В современной JavaScript-разработке сложные цепочки с промисами используются редко, так как они куда проще описываются при помощи генераторов с библиотекой co , которые рассмотрены в соответствующей главе. Можно сказать, что промисы лежат в основе более продвинутых способов асинхронной разработки.
Задачи
Промисифицировать setTimeout
Напишите функцию delay(ms) , которая возвращает промис, переходящий в состояние "resolved" через ms миллисекунд.
delay(1000) .then(() => alert("Hello!"))
Такая функция полезна для использования в других промис-цепочках.
Вот такой вызов:
return new Promise((resolve, reject) => < setTimeout(() =>< doSomeThing(); resolve(); >, ms) >);
Станет возможным переписать так:
return delay(ms).then(doSomething);