До сих пор ты работал только с обычными значениями. Создавал переменную или константу, сохранял туда значение и мог сразу его использовать, например вывести на экран.

Но что делать, если значение появляется не сразу, а спустя какое-то время? Данные мы часто получаем из базы данных или внешнего сервера. Эти операции занимают какое-то время и есть два способа работать с ними:

  • Мы можем попробовать заблокировать выполнение программы до тех пор, пока не получим данные.
  • Или мы можем продолжить выполнение программы, а с данными разобраться когда они появятся.

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

Если данные, которые ты ждешь критически важны для продвижения дальше, то нужно блокироваться и ничего с этим не сделать. А если можно отложить их обработку, то конечно не стоит терять время, ведь можно сделать еще что-то полезное.

Что такое JavaScript Promise

Promise - это специальный тип объектов, который помогает работать с асинхронными операциями.

Многие функции будут тебе возвращать промис в ситуациях, когда значение невозможно получить сразу.

const userCount = getUserCount();

console.log(userCount); // Promise { <pending> }

В этом случае getUserCount - это функция, которая возвращает Promise. Если мы попробуем сразу же вывести на экран значение переменной userCount, то получим что-то вроде Promise { <pending> }.

Это произойдет потому, что данных еще нет и нам нужно их ждать.

Состояния Promise в JS

Промис может быть в нескольких состояниях:

  • Pending - ответ еще не готов. Ждите.
  • Fulfilled - ответ готов, успех. Заберите данные.
  • Rejected - ответ готов, ошибка. Обработайте.

С состоянием pending мы не можем сделать ничего полезного, только ждать. А в других случаях, можем добавить функции обработчики, которые будут вызваны при переходе промиса в состояние fulfilled или rejected.

Для того, чтобы обработать успешное получение данных, нам нужна функция then.

const userCount = getUserCount();
const handleSuccess = (result) => {
  console.log(`Promise was fulfilled. Result is ${result}`);
}

userCount.then(handleSuccess);

А для обработки ошибок - catch.

const handleReject = (error) => {
  console.log(`Promise was rejected. The error is ${error}`);
}

userCount.catch(handleReject);

Обрати внимание, что функция getUserCount возвращает промис, поэтому мы не можем напрямую использовать userCount, а передаем в then и catch функции обработчики, которые будут вызваны в случае успеха или ошибки.

Функции then и catch можно вызвать последовательно. В таком случае мы учтем и ситуацию, когда все будет хорошо, и когда возникнет ошибка.

const userCount = getUserCount();
const handleSuccess = (result) => {
  console.log(`Promise was fulfilled. Result is ${result}`);
}

const handleReject = (error) => {
  console.log(`Promise was rejected. The error is ${error}`);
}

userCount.then(handleSuccess).catch(handleReject);

Обработка асинхронных ошибок в JavaScript Promise

Предположим, что у нас есть функция getUserData(userId), которая возвращает информацию о пользователе или бросает ошибку, если с параметром userId будут какие-то проблемы.

Раньше мы добавляли обычный try/catch и могли обработать ошибку в блоке catch.

try {
  console.log(getUserData(userId));
} catch (e) {
  handleError(e);
}

Но ошибки, которые возникают в асинхронном коде внутри промисов, невозможно поймать обычным try/catch.

Попробуем заменить блокирующую функцию getUserData(userId) , которая сразу возвращает результат на асинхронную fetchUserData(userId), которая возвращает промис.

Поведение мы хотим оставить тем же — вывести на экран результат в случае успеха, или обработать ошибку, если она возникнет.

try {
  fetchUserData(userId).then(console.log);
} catch (e) {
  handleError(e);
}

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

Для того, чтобы лучше понять порядок выполнения программы, добавим блок finally. Он выполнится в любом случае, но будет это до UnhandledPromiseRejection или после?

try {
  fetchUserData(userId).then(console.log);
} catch (e) {
  handleError(e);
} finally {
  console.log('finally');
}

Разберемся пошагово:

  1. В блоке try мы вызываем функцию fetchUserData, которая возвращает Promise в состоянии pending
  2. Блок catch игнорируется, потому что в блоке try ошибок не было. Асинхронное выполнение пока никого не интересует!
  3. На экран выводится строка finally
  4. Возникает ошибка в асинхронном коде и мы видим в консоли UnhandledPromiseRejectionWarning

Чтобы не возникало необработанных ошибок в промисах, к ним нужно всегда добавлять обработчики в .catch().

fetchUserData(userId).then(console.log).catch(handleError);

Код стал короче, чище и мы избавились от неожиданных ошибок, которые могут сломать наш код.