Промисы в JavaScript

Обещают что-то сделать и не блокируют код

Промисы в JavaScript

В JavaScript промис — это такая штука, которая обещает вернуть результат какого-то действия, но не сразу, а когда оно закончится. Промисы нужны, чтобы работать с асинхронными задачами — например, загрузкой данных с сервера, чтением файлов или чем-то ещё, что занимает время. Пока промис в ожидании, остальной код спокойно выполняется дальше и не зависает.

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

Введение в промисы

Когда мы пишем код на JavaScript, многие операции занимают время: загрузка данных с сервера, таймеры, запросы к API. Если бы сайт зависал, пока не придут данные, было бы плохо. Поэтому в JS есть механизмы, позволяющие запускать долгие процессы и при этом не тормозить выполнение остального кода. Один из таких инструментов — промисы (Promise).

Что такое промисы

Promise — это объект, который представляет результат асинхронной операции: либо успешный результат, либо ошибку. Проще говоря, это обещание, что код когда-нибудь выполнится и выдаст результат.

Промис может находиться в одном из трёх состояний:

  1. pending — ожидание (промис ещё не завершился);
  2. fulfilled — успешно завершён;
  3. rejected — ошибка.

Допустим, вы зашли в кофейню и заказали кофе. Бариста принимает заказ, но не выдаёт вам кофе сразу — его ещё нужно приготовить. Поэтому он даёт вам чек и тем самым обещает, что вы получите какой-то результат. Пока кофе готовится, вам необязательно быть у стойки — вы можете сесть за стол и почитать новую статью в «Коде». То есть, пока идёт ожидание, вы можете асинхронно выполнять другие задачи. И пока вы ждёте, происходит следующее:

  • Кофе в процессе приготовления → pending (ожидание).
  • Бариста ставит перед вами чашку → fulfilled (успех, заказ выполнен).
  • Ой, кофемашина взорвалась или кто-то утащил все зёрна → rejected (отказ, заказ не выполнен).

Промис в JavaScript работает по такому же принципу: он создаётся, ожидает результат и в итоге либо выполняется, либо завершается с ошибкой.

Теперь разберёмся, как создать такой объект в JavaScript.

Создание промисов

Промисы создаются с помощью конструктора Promise, который позволяет определить асинхронную операцию и её возможные исходы. 

Конструктор Promise

Когда мы создаём новый промис, ему передаётся функция-исполнитель (executor), которая сразу же запускается. Эта функция принимает два аргумента:

  • resolve — вызывает успешное выполнение промиса (fulfilled);
  • reject — сообщает об ошибке (rejected).

Синтаксис такой:

// Создаём через конструктор новый промис
const myPromise = new Promise((resolve, reject) => {
  // Здесь выполняется асинхронная операция
  if (успешное выполнение) {
    resolve(результат);
  } else {
    reject(ошибка);
  }
});

Дальше мы напишем функцию с промисом: посмотрим, как он работает изнутри, и правильно обработаем его результаты.

Синтаксис создания промиса

Допустим, у нас есть функция orderCoffee(), которая имитирует заказ кофе в кафе. Мы не знаем заранее, чем всё закончится: кофе может быть приготовлен успешно либо что-то пойдёт не так и сломается кофемашина.

Вот как это будет выглядеть в коде:

function orderCoffee() {
  console.log("Заказ принят! Ваш кофе готовится...");
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() < 0.8) {
        resolve("Ваш заказ готов!");
      } else {
        reject("Извините, у нас сломалась кофемашина");
      }
    // Имитация задержки в 2 секунды
    }, 2000); 
  });
}

Вот что мы здесь делаем:

  1. Создаём и сразу возвращаем новый Promise.
  2. Внутри промиса используем setTimeout(), чтобы имитировать задержку на приготовление.
  3. Через 2 секунды случайным образом решаем, успешно ли приготовлен кофе (resolve) или произошла ошибка (reject). В нашем условии с рандомом прописано, что кофе приготовится успешно с вероятностью 80%.

Если мы просто вызовем orderCoffee(), то увидим в консоли только «Заказ принят! Ваш кофе готовится…», но сам промис останется в ожидании (pending).

Промисы в JavaScript

Чтобы получить результат, нужно добавить обработчики .then() и .catch(). Сделаем это в следующем разделе.

Методы промисов

Когда асинхронная операция завершается, промис из состояния pending переходит в состояние успеха или ошибки.

Промисы в JavaScript

Для обработки этих результатов используются методы then(), catch() и finally().

Методы then и catch

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

Напишем обработчик успешного события промиса. Когда мы писали промис, то передали ему аргумент функции-исполнителя resolve с соответствующим сообщением «Ваш заказ готов!». Теперь передадим это сообщение в первый обработчик в цепочке then():

 .then((result) => {
   console.log(result);
 })

Для обработки состояния ошибки промиса используется метод catch(), который получает один аргумент, представляющий ошибку:

 .catch((error) => {
   console.error(error);
 });

Мы написали два обработчика, теперь добавим их к вызову нашей функции orderCoffee():

orderCoffee()
 .then((result) => {
   console.log(result);
 })
 .catch((error) => {
   console.error(error);
 });

Вызовем функцию вместе с обработчиками. Получаем первое сообщение, а затем ждём, пока выполняется асинхронная операция. Спустя 2 секунды в консоль выводится результат:

Промисы в JavaScript

Метод finally

Есть ещё метод finally(), и его можно использовать для выполнения кода, который должен быть исполнен после завершения промиса независимо от его разрешения или отклонения. Он полезен для выполнения заключительных действий в программе: очистки ресурсов, закрытия файловых потоков или скрытия индикаторов загрузки в пользовательском интерфейсе.

В нашем примере finally() мог бы выглядеть так:

orderCoffee()
  .then((result) => {
    console.log(result); 
  })
  .catch((error) => {
    console.error(error); 
  })
  .finally(() => {
    console.log("Актуальные акции ищите в нашем приложении!");
  });
Промисы в JavaScript

Часто все три метода объединяются в цепочки методов. Это позволяет создавать сложные последовательности асинхронных операций, где каждая следующая зависит от результата предыдущей. Об этом поговорим чуть позже.

Статические методы: Promise.all(), Promise.allSettled(), Promise.any(), Promise.race()

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

👉 Кстати, на собеседованиях часто спрашивают, как вручную реализовать тот или иной статический метод промиса, то есть написать полифил. Поэтому полезно разобраться, как под капотом работают эти методы.

Promise.all()

Ожидает выполнения всех промисов и возвращает массив их результатов. Если хоть один промис завершился с ошибкой, весь Promise.all() переходит в состояние rejected. Этот метод используют, когда все операции зависят друг от друга и нужна только полная успешная загрузка.

// Создаём три промиса, которые имитируют приготовление напитков
// Чай готовится 1 секунду
const p1 = new Promise(res => setTimeout(() => res("Чай"), 1000));  
// Кофе готовится 1,5 секунды
const p2 = new Promise(res => setTimeout(() => res("Кофе"), 1500)); 
// Сок выжимается за 2 секунды
const p3 = new Promise(res => setTimeout(() => res("Сок"), 2000)); 
// Используем Promise.all(), чтобы дождаться завершения всех промисов
Promise.all([p1, p2, p3])
// Выведет массив с готовыми напитками
  .then(results => console.log("Все напитки готовы:", results)) 
// Если хотя бы один промис завершится с ошибкой, сработает catch()
  .catch(error => console.error("Ошибка:", error));
Промисы в JavaScript

Если хотя бы один промис завершится с reject, Promise.all() сразу перейдёт в catch и проигнорирует остальные.

Promise.allSettled()

Ждёт выполнения всех промисов, но неважно, успешно или с ошибкой. Возвращает массив с результатами каждого промиса, где указано status: fulfilled или status: rejected.

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

const p1 = new Promise(res => setTimeout(() => res("Чай"), 1000));
const p2 = new Promise((_, rej) => setTimeout(() => rej("Кофе не получился"), 1500));
const p3 = new Promise(res => setTimeout(() => res("Сок"), 2000));
Promise.allSettled([p1, p2, p3])
  .then(results => console.log(results));

В результате получаем массив:

Промисы в JavaScript

В отличие от Promise.all(), ошибки не прерывают выполнение, а просто возвращаются в reason.

Promise.any()

Ожидает первый успешно выполненный промис и возвращает его результат. Если все промисы завершились ошибками, возвращает массив ошибок.

Используют, когда достаточно любого успешного результата.

const p1 = new Promise((_, rej) => setTimeout(() => rej("Ошибка 1"), 1000));
const p2 = new Promise(res => setTimeout(() => res("Успех 2"), 1500));
const p3 = new Promise((_, rej) => setTimeout(() => rej("Ошибка 3"), 2000));
Promise.any([p1, p2, p3])
  .then(result => console.log("Первый успешный:", result))
  .catch(error => console.error("Все промисы зафейлились:", error));
Промисы в JavaScript

Если все промисы завершатся с ошибкой, Promise.any() вернёт AggregateError (специальный объект, содержащий все ошибки).

Promise.race()

Ждёт первый завершившийся промис из списка и передаёт его результат дальше. Неважно, был ли этот промис успешным (fulfilled) или завершился с ошибкой (rejected) — Promise.race() просто берёт первый и завершает выполнение.

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

const p1 = new Promise(res => setTimeout(() => res("Успех 1"), 2000));
const p2 = new Promise((_, rej) => setTimeout(() => rej("Ошибка 2"), 1000));
const p3 = new Promise(res => setTimeout(() => res("Успех 3"), 1500));
Promise.race([p1, p2, p3])
  .then(result => console.log("Первый завершился:", result)) 
  .catch(error => console.error("Ошибка первой операции:", error));

В этом примере промис p2 завершается через 1 секунду с ошибкой, и поскольку он быстрее всех, то Promise.race() сразу завершится с rejected:

Промисы в JavaScript

Но если бы первым завершился успешный промис, то результат пошёл бы в then().

👉 Promise.race() не дожидается остальных промисов, а просто берёт первый завершившийся. Если важно получить все результаты, даже если некоторые завершились с ошибкой, лучше использовать Promise.allSettled().

Цепочки промисов

Часто then, catch, finally объединяются в цепочки методов. Это позволяет создавать сложные последовательности асинхронных операций, где каждая следующая зависит от результата предыдущей.

Передача данных по цепочке промисов

Каждый then() получает данные от предыдущего промиса и передаёт их дальше.

Допустим, бариста сначала принимает заказ, затем готовит кофе, добавляет сахар и в конце подаёт кофе клиенту — без успешного выполнения предыдущей операции следующая невозможна:

takeOrder()
  .then(order => {
    console.log(`Заказ принят: ${order}`);
    // Готовим кофе
    return makeCoffee(order); 
  })
  .then(coffee => {
    console.log(`Кофе готов: ${coffee}`);
    // Добавляем сахар
    return addSugar(coffee); 
  })
  .then(sweetCoffee => {
    console.log(`Добавлен сахар: ${sweetCoffee}`);
    // Подаём клиенту
    return serveCoffee(sweetCoffee); 
  })
  .then(finalDrink => {
    console.log(`Подано клиенту: ${finalDrink}`);
  })
  .catch(error => {
    console.error("Что-то пошло не так:", error);
  });

Такой подход делает код читаемым и последовательным, без сложных вложенностей — сразу понятно, что за чем идёт. С промисами мы поэтапно описываем процесс, а ошибки ловятся в одном catch(), что упрощает отладку.

Обработка ошибок в цепочках

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

orderCoffee()
// Движемся по цепочке промисов
  .then(coffee => {
    console.log(`Получен заказ: ${coffee}`);
    return addSugar(coffee);
  })
  .then(sweetCoffee => {
    console.log(`Добавлен сахар: ${sweetCoffee}`);
    return serveCoffee(sweetCoffee);
  })
  .then(finalDrink => {
    console.log(`Подано клиенту: ${finalDrink}`);
  })
// Если где-то ошибка, то код переходит сюда
  .catch(error => {
    console.error("Что-то пошло не так:", error);
  })
// Выполняется в любом случае
  .finally(() => {
    console.log("Рабочий день бариста продолжается...");
  });

Суть в следующем:

  • catch() перехватит ошибку из любого предыдущего then.
  • Если ошибка случится на любом этапе (orderCoffee(), addSugar(), serveCoffee()), выполнение цепочки остановится, и код перейдёт в catch().
  • finally() выполнится в любом случае — независимо от успеха или ошибки.

Промисы в реальной разработке

Промисы используются везде, где нужно дождаться результата асинхронной операции: запросы к серверу, работа с файлами, задержки в анимациях. Они делают код чище и удобнее, а большинство современных API уже работают на промисах.

Сценарии могут быть такими:

  • Запросы на сервер (fetch, axios) — самый частый кейс. Любая загрузка данных с API, отправка форм или обновление информации на странице требуют асинхронного запроса, чтобы не блокировать работу приложения.
  • Чтение и обработка файлов — если пользователю нужно загрузить изображение или обработать данные из файла, это тоже происходит асинхронно. В браузере это реализовано через File API, а в Node.js — через модуль fs.
  • Таймеры и анимации — плавные эффекты, задержки перед выполнением действия, работа с setTimeout и requestAnimationFrame. Например, можно показать анимацию загрузки перед тем, как появятся данные.
  • Работа с IndexedDB — браузерное хранилище данных, где можно сохранять кэшированные файлы или пользовательские настройки. Операции с базой происходят асинхронно, и промисы помогают удобно их обрабатывать.
  • Взаимодействие с устройствами — промисы используются при работе с Bluetooth, Geolocation API (получение местоположения пользователя), WebRTC (передача видео и аудио в реальном времени) и другими API.

Дальше посмотрим реальный пример.

Пример использования промисов

Допустим, у нас есть погодное приложение, которое получает температуру в нужном городе. Код, если что, рабочий, можете потестить со своим городом:

function getWeather(city) {
  const apiKey = "your-api-key"; 
// Формируем URL с нужным городом и ключом API
  const apiUrl = `https://api.weatherapi.com/v1/current.json?key=${apiKey}&q=${city}`; 
// Делаем асинхронный запрос на сервер
  return fetch(apiUrl) 
    .then(response => {
  // Проверяем, не вернул ли сервер ошибку
      if (!response.ok) { 
   // Если да, то вызываем ошибку
        throw new Error("Ошибка при получении данных"); 
      }
    // Преобразуем ответ в JSON
      return response.json(); 
    })
    .then(data => {
      // Достаём из ответа нужные данные: город, температуру и описание погоды
      return `Температура в ${data.location.name}: ${data.current.temp_c}°C, ${data.current.condition.text}`;
    });
}
// Используем функцию и обрабатываем промис
getWeather("Hanoi")
// Если запрос успешен, выводим данные в консоль
  .then(result => console.log(result)) 
// Если произошла ошибка, выводим её в консоль
  .catch(error => console.error("Ошибка:", error));

Здесь происходит следующее:

  1. Отправляем HTTP-запрос с помощью fetch(), который возвращает промис.
  2. Когда сервер отвечает, промис разрешается, и в then() мы получаем объект response.
  3. Проверяем response.ok:
  4. Если сервер вернул успешный ответ (код 200–299), превращаем его в JSON и передаём дальше.
  5. Если сервер вернул ошибку (404, 500 и так далее), создаём исключение с throw new Error(), и код переходит в catch().
  6. Если произошла сетевая ошибка (например, нет интернета, сервер недоступен), fetch() сразу переходит в catch(), минуя then().
  7. В catch() обрабатываем все возможные ошибки, чтобы код не сломался.
  8. Вызываем функцию и выводим результат в консоль.

Такой код не блокирует интерфейс, потому что fetch() работает асинхронно, мы можем легко подставить любой город, не меняя код. А если возникнет ошибка, то сработает catch(), и приложение не упадёт.

Распространённые ошибки

Хотя промисы делают код чище и удобнее, неправильное их использование может привести к багам и проблемам. Разберём несколько типичных ошибок.

🚫 Не обрабатывать ошибки промисов

Если промис завершится с ошибкой, но при этом не добавлен метод catch(), браузер покажет предупреждение UnhandledPromiseRejectionWarning, а в продакшене это может привести к падению сервера.

//❌ Плохо
fetch("https://example.com/api") 
// Ошибки не обрабатываются!
  .then(response => response.json());

// ✅ Хорошо
fetch("https://example.com/api")
  .then(response => response.json())
  .catch(error => console.error("Ошибка запроса:", error));

🚫 Использовать try/catch внутри промиса

Некоторые разработчики помещают try/catch внутрь new Promise(), но это не имеет смысла. Промис уже умеет перехватывать ошибки и передавать их в catch(), так что лишний try/catch только усложняет код.

// ❌ Неправильно
new Promise((resolve, reject) => {
  try {
    const data = someFunction();
    resolve(data);
  } catch (e) {
    reject(e);
  }
})
  .then(data => console.log(data))
  .catch(error => console.error(error));

// ✅ Правильно
new Promise((resolve, reject) => {
  const data = someFunction();
 // Ошибка и так пойдёт в catch()
  resolve(data); 
})
  .then(data => console.log(data))
  .catch(error => console.error(error));

🚫  Неправильно использовать Promise.race()

Promise.race() возвращает результат самого первого выполненного промиса, но если не обработать reject(), это может привести к пропущенным ошибкам.

В этом коде:

Promise.race([
  fetch("https://slow.api.com"),
  fetch("https://fast.api.com"),
]).then(response => console.log(response));

Если один промис завершается первым, результат этого промиса передаётся в then(), а второй промис не обрабатывается вообще. 

Если первый промис (fast.api.com) успешный, но второй (slow.api.com) завершится с ошибкой, мы её не увидим. Promise.race() просто игнорирует остальные промисы после первого завершённого. Если оба промиса ошибочные, но один завершается быстрее, будет обработана только его ошибка. Вторая ошибка не попадёт в catch().

Поэтому если важно видеть все результаты, то лучше использовать метод Promise.allSettled(), который ждёт все промисы и возвращает их статусы:

Promise.allSettled([
  fetch("https://slow.api.com"),
  fetch("https://fast.api.com"),
]).then(results => console.log(results));

Сравнение промисов с другими асинхронными паттернами

Функции обратного вызова

Один из способов работы с асинхронностью — использовать обратные вызовы (колбэки, callbacks). В этом случае одна функция передаётся в другую в виде аргумента и выполняется позже — после того как завершится какая-то асинхронная операция. 

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

getData(function(a){
    getMoreData(a, function(b){
        getMoreData(b, function(c){
            getMoreData(c, function(d){
                getMoreData(d, function(e){
                    // Спустились на пятый уровень вложенности
                    console.log(e); // Только здесь используем полученные данные
                });
            });
        });
    });
});

Здесь пять функций получения данных. Каждый новый уровень вложенности добавляет сложности, и для выполнения каждого нового запроса данных нужно ждать завершения предыдущего. Поддерживать и рефакторить такой код сложно. Именно поэтому и появились промисы:

// Новый подход с промисами
getData()
    .then(a => getMoreData(a))
    .then(b => getMoreData(b))
    .then(c => console.log(c))
    .catch(console.error);

Но колбэки не ушли полностью — есть случаи, когда они могут быть полезны. Часто их используют для простых одноразовых асинхронных операций или в старом коде или API, где ещё нет поддержки промисов.

События

Другой способ работы с асинхронностью — события (events). В этом случае код не ждёт завершения операции, а реагирует на неё, когда она происходит. 

JavaScript использует Event Loop — механизм, который управляет выполнением кода и обработкой событий. Когда происходит событие (например, клик по кнопке), его обработчик попадает в очередь (Event Queue) и выполняется, когда стек вызовов (Call Stack) становится пустым.

Например, у нас есть кнопка:

button.addEventListener("click", () => {
  console.log("Кнопка нажата!");
});

Браузер не вызывает обработчик сразу, а ждёт, пока пользователь кликнет по кнопке. Когда это случится, Event Loop добавит обработчик в очередь, и он выполнится, как только освободится главный поток..

Обычно события используют для пользовательских взаимодействий (нажатия кнопок, загрузка файлов) и когда нет строгой последовательности операций — просто реагируем на события.

Вам слово

Приходите к нам в соцсети поделиться своим мнением о статье и почитать, что пишут другие. А ещё там выходит дополнительный контент, которого нет на сайте: шпаргалки, опросы и разная дурка. В общем, вот тележка, вот ВК — велком!

Обложка:

Алексей Сухов

Корректор:

Елена Грицун

Вёрстка:

Кирилл Климентьев

Соцсети:

Юлия Зубарева

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
А вы читали это?
hard