Promise в JavaScript простыми словами — полное руководство

JavaScript
130 просмотров

Представьте: вы заказали пиццу. Курьер говорит «буду через 30 минут» и уходит. Вы не стоите у двери всё это время — занимаетесь своими делами, а когда звонит домофон, открываете и берёте заказ. Или расстраиваетесь, если курьер позвонил и сказал «ресторан закрыт». Это и есть Promise в JavaScript — обещание, которое либо выполнится, либо нет. В этой статье разберём js promise с нуля: что это, как работают промисы JS, и зачем они вообще нужны.

Promise в JavaScript — промисы простыми словами

Зачем нужны промисы — проблема без них

В JavaScript многие операции занимают время: запросы к серверу, чтение файлов, таймеры. Раньше для обработки таких операций использовали колбэки — функции, которые вызываются, когда результат готов. Выглядело это примерно так:

// Три последовательных запроса через колбэки
getUser(userId, function(user) {
    getOrders(user.id, function(orders) {
        getOrderDetails(orders[0].id, function(details) {
            // вот теперь можно что-то сделать
            console.log(details);
        }, function(err) {
            console.log('Ошибка деталей:', err);
        });
    }, function(err) {
        console.log('Ошибка заказов:', err);
    });
}, function(err) {
    console.log('Ошибка пользователя:', err);
});

У этого явления даже есть имя — callback hell, или «ад колбэков». Код уходит вправо пирамидой, читать его мучительно, обрабатывать ошибки приходится отдельно на каждом уровне. Промисы решают эту проблему элегантно.

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

Promise (промис, «обещание») — это специальный объект JavaScript, который представляет результат асинхронной операции. Он как квиток из химчистки: вы сдали куртку, получили бумажку-обещание, и когда придёте через три дня — либо заберёте чистую куртку, либо вам скажут «увы, не получилось».

Промис существует в одном из трёх состояний:

  • Pending (ожидание) — операция ещё выполняется.
  • Fulfilled (выполнен) — операция завершилась успешно, есть результат.
  • Rejected (отклонён) — что-то пошло не так, есть причина ошибки.
Из состояния «pending» промис переходит в «fulfilled» или «rejected» ровно один раз и навсегда. Передумать нельзя — как выданный квиток не отзовёшь.

Промисы используются буквально везде в современной веб-разработке. Вот самые типичные ситуации:

  1. Запросы к серверу. Загрузить список товаров, отправить форму, получить данные пользователя — любой HTTP-запрос через fetch или Axios возвращает промис.
  2. Работа с файлами в Node.js. Читать и писать файлы на диске — операции небыстрые, поэтому они асинхронные и возвращают промисы.
  3. Запросы к базе данных. Найти пользователя, сохранить заказ, обновить запись — все современные ORM (Prisma, Mongoose, Sequelize) работают через промисы.
  4. Геолокация. Получить координаты пользователя через браузерный API — это тоже промис: пользователь может разрешить или отклонить запрос.
  5. Загрузка изображений и медиа. Дождаться, пока картинка полностью загрузится, прежде чем показывать её.
  6. Таймеры и паузы. Самодельная функция sleep() — пауза между шагами анимации или повторными попытками запроса — строится на промисе.
  7. IndexedDB и localStorage-обёртки. Хранение данных прямо в браузере через современные библиотеки — тоже промисы.
  8. Web Workers. Передача сообщений между основным потоком и фоновым воркером удобно оборачивается в промис.

Создаём Promise — new Promise()

Промис создаётся через конструктор new Promise() с функцией-исполнителем внутри. Эта функция получает два параметра: resolve (вызвать при успехе) и reject (вызвать при ошибке):

const myPromise = new Promise((resolve, reject) => {
    // имитируем запрос к серверу — занимает 1 секунду
    setTimeout(() => {
        const success = true; // пусть всё прошло хорошо

        if (success) {
            resolve('Данные получены!'); // передаём результат
        } else {
            reject('Сервер недоступен'); // передаём причину ошибки
        }
    }, 1000);
});

Обратите внимание: функция-исполнитель запускается сразу при создании промиса. А вот обработка результата — через .then() и .catch() — происходит потом.

.then() и .catch() — получаем результат

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

myPromise
    .then(result => {
        console.log('Успех:', result); // 'Успех: Данные получены!'
    })
    .catch(error => {
        console.log('Ошибка:', error); // сюда попадём, если reject()
    });

Можно также добавить .finally() — он выполнится в любом случае, и при успехе, и при ошибке. Удобно для скрытия лоадера или освобождения ресурсов:

showLoader(); // показываем спиннер

fetch('/api/data')
    .then(res => res.json())
    .then(data => renderData(data))
    .catch(err => showError(err))
    .finally(() => hideLoader()); // скрываем спиннер в любом случае

Цепочки промисов — .then().then().then()

Каждый .then() возвращает новый промис, поэтому вызовы можно выстраивать в цепочку. Это главное преимущество перед колбэками — код читается сверху вниз, как обычный последовательный:

// Тот же пример с тремя запросами — но теперь читаемо
getUser(userId)
    .then(user => getOrders(user.id))
    .then(orders => getOrderDetails(orders[0].id))
    .then(details => {
        console.log(details);
    })
    .catch(err => {
        // одна точка обработки ошибок для всей цепочки
        console.log('Что-то пошло не так:', err);
    });

Важный момент: если в .then() вернуть значение — оно передаётся в следующий .then() . Если вернуть промис — следующий шаг дождётся его выполнения. Это и позволяет делать последовательные асинхронные операции.

Цепочка промисов Promise JavaScript .then().catch() — схема выполнения

async/await — промисы без .then()

Синтаксис async/await — это не замена промисам, а более удобный способ их писать. Под капотом всё те же промисы, просто код выглядит как обычный синхронный. Функция с async всегда возвращает промис, а await внутри неё приостанавливает выполнение до получения результата:

// С .then() — промисная цепочка
function loadUser(id) {
    return getUser(id)
        .then(user => getOrders(user.id))
        .then(orders => orders[0]);
}

// С async/await — тот же результат, читается как синхронный код
async function loadUser(id) {
    const user = await getUser(id);
    const orders = await getOrders(user.id);
    return orders[0];
}

Ошибки с async/await перехватывают через стандартный try/catch — как в обычном синхронном коде:

async function loadData() {
    try {
        const response = await fetch('/api/data');
        const data = await response.json();
        return data;
    } catch (err) {
        console.log('Ошибка загрузки:', err);
    }
}
await работает только внутри async-функции. На верхнем уровне модуля он тоже работает, но в обычном скрипте — нет. Если забыть написать async — получите ошибку «await is not defined». Привет от JavaScript.

Promise.all — запускаем несколько промисов параллельно

Если нужно дождаться нескольких независимых операций одновременно — Promise.all запустит их параллельно и вернёт массив результатов. Это быстрее, чем ждать каждый по очереди:

// Последовательно — долго: 1 сек + 2 сек + 1.5 сек = 4.5 сек
const user = await getUser(id);         // 1 сек
const orders = await getOrders(id);     // 2 сек
const settings = await getSettings(id); // 1.5 сек

// Параллельно с Promise.all — быстро: ~2 сек (по самому долгому)
const [user, orders, settings] = await Promise.all([
    getUser(id),
    getOrders(id),
    getSettings(id)
]);

Реальный пример — загрузить данные для страницы профиля с нескольких эндпоинтов сразу:

async function loadProfilePage(userId) {
    const [profile, posts, followers] = await Promise.all([
        fetch(`/api/users/${userId}`).then(r => r.json()),
        fetch(`/api/users/${userId}/posts`).then(r => r.json()),
        fetch(`/api/users/${userId}/followers`).then(r => r.json()),
    ]);

    renderProfile(profile, posts, followers);
}
Важная деталь Promise.all: если хотя бы один промис завершится с ошибкой — весь Promise.all отклоняется немедленно. Остальные промисы продолжают выполняться, но их результат уже игнорируется. Это называется «fail fast» — упасть быстро.

Promise.allSettled — ждём всех, даже если есть ошибки

Когда нужно получить результаты всех промисов независимо от того, некоторые завершились ошибкой — используют Promise.allSettled (ES2020). Он никогда не отклоняется: возвращает массив объектов со статусом каждого промиса:

const results = await Promise.allSettled([
    fetch('/api/users'),      // допустим, успех
    fetch('/api/orders'),     // допустим, ошибка сети
    fetch('/api/products'),   // успех
]);

results.forEach(result => {
    if (result.status === 'fulfilled') {
        console.log('Успех:', result.value);
    } else {
        console.log('Ошибка:', result.reason);
    }
});

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

Promise.race — кто первый, тот и победил

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

function timeout(ms) {
    return new Promise((_, reject) =>
        setTimeout(() => reject(new Error('Превышено время ожидания')), ms)
    );
}

// Либо fetch вернёт данные, либо через 5 сек сработает таймаут
const data = await Promise.race([
    fetch('/api/slow-endpoint').then(r => r.json()),
    timeout(5000)
]);

Промисы в Node.js

В Node.js промисы используются повсеместно. Встроенные модули fs, http, crypto — изначально колбэчные — теперь имеют промисные версии. Работа с файловой системой через fs.promises :

const fs = require('fs').promises;
// или в ES-модулях:
import { readFile, writeFile } from 'fs/promises';

async function processFile() {
    try {
        const content = await readFile('input.txt', 'utf-8');
        const processed = content.toUpperCase();
        await writeFile('output.txt', processed);
        console.log('Файл обработан');
    } catch (err) {
        console.error('Ошибка:', err.message);
    }
}

Для работы с базами данных, HTTP-запросами, очередями — все современные Node.js библиотеки (Axios, Mongoose, Prisma, ioredis) возвращают промисы и поддерживают async/await из коробки.

// Пример с Axios — HTTP-клиент для Node.js
const axios = require('axios');

async function getWeather(city) {
    const { data } = await axios.get('https://api.example.com/weather', {
        params: { city }
    });
    return data;
}

// Параллельная загрузка нескольких городов
const [moscow, spb] = await Promise.all([
    getWeather('Moscow'),
    getWeather('Saint-Petersburg')
]);

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

Несколько граблей, на которые наступают почти все:

Забытый return в цепочке. Если не вернуть промис из .then() , следующий шаг получит undefined :

// ❌ Забыли return — следующий .then получит undefined
getUser(id)
    .then(user => {
        getOrders(user.id); // нет return!
    })
    .then(orders => console.log(orders)); // undefined

// ✅ Правильно
getUser(id)
    .then(user => {
        return getOrders(user.id); // или: user => getOrders(user.id)
    })
    .then(orders => console.log(orders));

Потерянный .catch(). Промис без обработчика ошибок «проглатывает» их молча — в консоли увидите предупреждение «Unhandled Promise Rejection»:

// ❌ Ошибка никуда не уйдёт — «тихая» поломка
fetch('/api/data')
    .then(res => res.json())
    .then(data => render(data));

// ✅ Всегда добавляйте .catch()
fetch('/api/data')
    .then(res => res.json())
    .then(data => render(data))
    .catch(err => console.error(err));

Последовательный await вместо параллельного. Использование await для независимых запросов одного за другим — самая частая причина медленного кода:

// ❌ Медленно: ждём каждый запрос по очереди
const a = await fetchA(); // 1 сек
const b = await fetchB(); // ещё 1 сек — итого 2 сек

// ✅ Быстро: запускаем параллельно
const [a, b] = await Promise.all([fetchA(), fetchB()]); // ~1 сек

Итого: Promise JavaScript — что запомнить

Промисы — это способ работать с асинхронным кодом без «ада колбэков». Они дают предсказуемую структуру: либо результат, либо ошибка, и всё это обрабатывается в одном месте.

  • new Promise(resolve, reject) — создать промис вручную.
  • .then() / .catch() / .finally() — обработать результат.
  • async/await — синтаксический сахар, делает код линейным.
  • Promise.all — параллельно, падает если хоть один упал.
  • Promise.allSettled — параллельно, ждёт всех, отдаёт статусы.
  • Promise.race — побеждает самый быстрый.

Промисы тесно связаны с таймерами — под капотом функции sleep() тоже лежит промис. Если вы ещё не читали про setTimeout и паузы в JavaScript — загляните в эту статью, там разбираем, как работают таймеры и почему JS не умеет «спать» как другие языки.

Вам может быть интересно:
Swiper.js — слайдер для сайта: настройки и примеры
Swiper.js — как подключить и настроить слайдер на сайте
Что такое функция в программировании — типы и примеры на JavaScript и Python
Что такое функция в программировании?
Анимация частиц и GLSL-шейдеры в PixiJS 8 — живой пример в браузере
Изучаем PixiJS. Часть 2 — продвинутая анимация и GLSL-шейдеры
Слайдер для сайта на Owl Carousel 2 — настройка и примеры
Слайдер для сайта Owl Carousel 2 — настройка просто и быстро
Комментарии 0 Разные мнения приветствуются. Здесь можно спорить и обсуждать, но уважительно и без токсичности.
Отмена