Промисы в 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 добавит обработчик в очередь, и он выполнится, как только освободится главный поток.

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

Часто задаваемые вопросы:

Какие состояния бывают у Promise?

Promise имеет три состояния:

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

Promise может сменить состояние только один раз, перейдя из pending в fulfilled или rejected.

Как использовать then() для обработки Promise?

Метод then() используется для обработки успешного результата промиса. Он принимает функцию обратного вызова, которая запускается после успешного выполнения промиса. Для обработки ошибок используется catch(), которая принимает функцию с ошибкой. Пример:

promise.then(result => {...}).catch(error => {...});

Что такое Promise.resolve() и Promise.reject()?

Promise.resolve() создаёт промис, который сразу переходит в состояние fulfilled с переданным значением. Promise.reject() создаёт промис с состоянием rejected и переданной причиной ошибки.

Как обработать несколько Promise одновременно?

Используют статические методы:

  • Promise.all() ждёт успешного завершения всех промисов и возвращает массив их результатов. Если один из промисов завершился с ошибкой — общий промис отклоняется.
  • Promise.allSettled() ждёт завершения всех промисов и возвращает массив с их статусами (fulfilled или rejected), независимо от результата.
  • Promise.any() ждёт первого успеха из промисов, если все завершились с ошибкой — возвращает AggregateError.
  • Promise.race() возвращает результат или ошибку первого завершённого промиса, игнорируя остальные.

Что такое цепочка промисов (Promise chaining)?

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

Вам слово

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

Обложка:

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

Корректор:

Елена Грицун

Вёрстка:

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

Соцсети:

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

Вам может быть интересно
hard