Промис (Promise) — это специальный объект в JavaScript, представляющий будущее завершение (или неудачу) асинхронной операции и её итоговое значение. По сути, промис — это заглушка (placeholder) для значения, которое пока недоступно, но будет доступно в будущем.
Представьте промис как заказ пиццы: вы не получаете её сразу, но курьер обещает привезти её позже. Вы не знаете точно когда, но знаете, что исход будет либо «пицца доставлена», либо «что-то пошло не так».
Промис может находиться в одном из трёх состояний:
- Pending (в ожидании): начальное состояние, когда асинхронная операция ещё выполняется.
- Fulfilled (выполнен): операция успешно завершилась, и промис теперь разрешён (resolved) со значением.
- Rejected (отклонён): операция провалилась, и промис завершён с причиной (обычно ошибкой).
Когда вы заказываете пиццу, вы в состоянии pending — голодны и полны надежд. Если пицца приезжает горячей и с сыром, вы перешли в состояние fulfilled. А если из ресторана звонят и говорят, что уронили вашу пиццу на пол, вы в состоянии rejected.
Независимо от того, закончится ли ваш ужин радостью или разочарованием, как только есть окончательный исход, промис считается settled (завершённым).
Один из самых распространённых способов создать промис — конструктор new Promise(). Конструктор принимает функцию с двумя параметрами: resolve и reject. Эти функции используются, чтобы перевести промис из состояния pending в fulfilled или rejected.
Если внутри функции-исполнителя (executor) выбрасывается ошибка, промис будет отклонён с этой ошибкой.
Возвращаемое значение функции-исполнителя игнорируется: чтобы завершить промис, следует использовать только resolve или reject.
const myPromise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve('Operation was successful!');
} else {
reject('Something went wrong.');
}
});В примере выше:
- Если условие
successравноtrue, промис выполняется (fulfilled), и значение'Operation was successful!'передаётся в функциюresolve. - Если условие
successравноfalse, промис отклоняется (rejected), и ошибка'Something went wrong.'передаётся в функциюreject.
Как только промис создан, вы можете обрабатывать исход с помощью методов .then(), .catch() и .finally().
.then()используется для обработки выполненного (fulfilled) промиса и доступа к его результату..catch()используется для обработки отклонённого (rejected) промиса и перехвата любых возникших ошибок..finally()используется для обработки завершённого (settled) промиса, независимо от того, разрешился он или был отклонён.
const myPromise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve('Operation was successful!');
} else {
reject('Something went wrong.');
}
});
myPromise
.then(result => {
console.log(result); // Выполнится, если промис fulfilled
})
.catch(error => {
console.error(error); // Выполнится, если промис rejected
})
.finally(() => {
console.log('The promise has completed'); // Выполнится, когда промис settled
});Одна из замечательных особенностей промисов в том, что они позволяют сцеплять несколько асинхронных операций в цепочку. Когда вы сцепляете промисы, каждый блок .then() ждёт завершения предыдущего, прежде чем выполниться.
const { setTimeout: delay } = require('node:timers/promises');
const promise = delay(1000).then(() => 'First task completed');
promise
.then(result => {
console.log(result); // 'First task completed'
return delay(1000).then(() => 'Second task completed'); // Возвращаем второй промис
})
.then(result => {
console.log(result); // 'Second task completed'
})
.catch(error => {
console.error(error); // Если какой-либо промис отклонён, ловим ошибку
});Один из лучших способов работать с промисами в современном JavaScript — async/await. Он позволяет писать асинхронный код, который выглядит синхронно, что делает его гораздо легче читать и сопровождать.
asyncиспользуется, чтобы определить функцию, возвращающую промис.awaitиспользуется внутриasync-функции, чтобы приостановить выполнение до завершения промиса.
async function performTasks() {
try {
const result1 = await promise1;
console.log(result1); // 'First task completed'
const result2 = await promise2;
console.log(result2); // 'Second task completed'
} catch (error) {
console.error(error); // Ловит любое отклонение или ошибку
}
}
performTasks();В функции performTasks ключевое слово await гарантирует, что каждый промис завершится, прежде чем перейти к следующей инструкции. Это делает поток асинхронного кода более линейным и читаемым.
По сути, код выше выполнится так же, как если бы пользователь написал:
promise1
.then(function (result1) {
console.log(result1);
return promise2;
})
.then(function (result2) {
console.log(result2);
})
.catch(function (error) {
console.log(error);
});При использовании ES-модулей сам модуль рассматривается как область верхнего уровня (top-level scope), которая нативно поддерживает асинхронные операции. Это значит, что можно использовать await на верхнем уровне без необходимости в async-функции.
import { setTimeout as delay } from 'node:timers/promises';
await delay(1000);Async/await может быть куда более замысловатым, чем приведённые простые примеры. У Джеймса Снелла (James Snell), члена Технического руководящего комитета Node.js, есть подробная презентация, исследующая тонкости промисов и async/await.
Node.js предоставляет версии на основе промисов для многих своих ключевых API, особенно там, где асинхронные операции традиционно обрабатывались колбэками. Это упрощает работу с API Node.js и промисами и снижает риск «callback hell» (ада колбэков).
Например, у модуля fs (файловая система) есть API на основе промисов под fs.promises:
const fs = require('node:fs').promises;
// Или можно импортировать промисифицированную версию напрямую:
// const fs = require('node:fs/promises');
async function readFile() {
try {
const data = await fs.readFile('example.txt', 'utf8');
console.log(data);
} catch (err) {
console.error('Error reading file:', err);
}
}
readFile();В этом примере fs.readFile() возвращает промис, который мы обрабатываем синтаксисом async/await, чтобы асинхронно прочитать содержимое файла.
Глобальный объект Promise в JavaScript предоставляет несколько мощных методов, помогающих эффективнее управлять множеством асинхронных задач:
Promise.all(): voidЭтот метод принимает массив промисов и возвращает новый промис, который разрешается, как только все промисы выполнены. Если какой-либо промис отклоняется, Promise.all() немедленно отклонится. Однако даже при отклонении промисы продолжают выполняться. При обработке большого числа промисов, особенно в пакетной обработке (batch processing), использование этой функции может нагружать память системы.
const { setTimeout: delay } = require('node:timers/promises');
const fetchData1 = delay(1000).then(() => 'Data from API 1');
const fetchData2 = delay(2000).then(() => 'Data from API 2');
Promise.all([fetchData1, fetchData2])
.then(results => {
console.log(results); // ['Data from API 1', 'Data from API 2']
})
.catch(error => {
console.error('Error:', error);
});Promise.allSettled(): voidЭтот метод ждёт, пока все промисы разрешатся или отклонятся, и возвращает массив объектов, описывающих исход каждого промиса.
const promise1 = Promise.resolve('Success');
const promise2 = Promise.reject('Failed');
Promise.allSettled([promise1, promise2]).then(results => {
console.log(results);
// [ { status: 'fulfilled', value: 'Success' }, { status: 'rejected', reason: 'Failed' } ]
});В отличие от Promise.all(), Promise.allSettled() не «замыкается» (short-circuit) при неудаче. Он ждёт завершения всех промисов, даже если некоторые отклоняются. Это даёт лучшую обработку ошибок для пакетных операций, когда вы можете захотеть узнать статус всех задач, независимо от неудач.
Promise.race(): voidЭтот метод разрешается или отклоняется, как только завершится первый промис — неважно, разрешится он или отклонится. Независимо от того, какой промис завершится первым, все промисы выполняются полностью.
const { setTimeout: delay } = require('node:timers/promises');
const task1 = delay(2000).then(() => 'Task 1 done');
const task2 = delay(1000).then(() => 'Task 2 done');
Promise.race([task1, task2]).then(result => {
console.log(result); // 'Task 2 done' (поскольку task2 завершается первым)
});Promise.any(): voidЭтот метод разрешается, как только разрешается один из промисов. Если все промисы отклонены, он отклонится с AggregateError.
const { setTimeout: delay } = require('node:timers/promises');
const api1 = delay(2000).then(() => 'API 1 success');
const api2 = delay(1000).then(() => 'API 2 success');
const api3 = delay(1500).then(() => 'API 3 success');
Promise.any([api1, api2, api3])
.then(result => {
console.log(result); // 'API 2 success' (поскольку разрешается первым)
})
.catch(error => {
console.error('All promises rejected:', error);
});Эти методы напрямую создают отклонённый или разрешённый промис.
Promise.resolve('Resolved immediately').then(result => {
console.log(result); // 'Resolved immediately'
});Promise.try(): voidPromise.try() — это метод, который выполняет заданную функцию, синхронную или асинхронную, и оборачивает результат в промис. Если функция выбрасывает ошибку или возвращает отклонённый промис, Promise.try() вернёт отклонённый промис. Если функция завершается успешно, возвращённый промис будет выполнен с её значением.
Это особенно полезно для единообразного старта цепочек промисов, особенно при работе с кодом, который может выбрасывать ошибки синхронно.
function mightThrow() {
if (Math.random() > 0.5) {
throw new Error('Oops, something went wrong!');
}
return 'Success!';
}
Promise.try(mightThrow)
.then(result => {
console.log('Result:', result);
})
.catch(err => {
console.error('Caught error:', err.message);
});В этом примере Promise.try() гарантирует, что если mightThrow() выбросит ошибку, она будет поймана в блоке .catch(), что упрощает обработку и синхронных, и асинхронных ошибок в одном месте.
Promise.withResolvers(): voidЭтот метод создаёт новый промис вместе со связанными с ним функциями resolve и reject и возвращает их в удобном объекте. Это используется, например, когда нужно создать промис, но разрешить или отклонить его позже — извне функции-исполнителя.
const { promise, resolve, reject } = Promise.withResolvers();
setTimeout(() => {
resolve('Resolved successfully!');
}, 1000);
promise.then(value => {
console.log('Success:', value);
});В этом примере Promise.withResolvers() даёт вам полный контроль над тем, когда и как промис будет разрешён или отклонён, без необходимости определять функцию-исполнитель inline. Этот паттерн часто используется в событийно-ориентированном программировании, при таймаутах или при интеграции с API, не основанными на промисах.
Обработка ошибок в промисах гарантирует, что ваше приложение ведёт себя корректно в случае неожиданных ситуаций.
- Можно использовать
.catch(), чтобы обработать любые ошибки или отклонения, возникшие во время выполнения промисов.
myPromise
.then(result => console.log(result))
.catch(error => console.error(error)) // Обрабатывает отклонение
.finally(() => console.log('Promise completed')); // Выполняется независимо от разрешения промиса- Как вариант, при использовании
async/awaitможно применять блокtry/catch, чтобы ловить и обрабатывать ошибки.
async function performTask() {
try {
const result = await myPromise;
console.log(result);
} catch (error) {
console.error(error); // Обрабатывает любые ошибки
} finally {
// Этот код выполняется независимо от неудачи
console.log('performTask() completed');
}
}
performTask();Помимо промисов, Node.js предоставляет несколько других механизмов для планирования задач в event loop.
queueMicrotask(): voidqueueMicrotask() используется для планирования микрозадачи (microtask) — лёгкой задачи, которая выполняется после текущего исполняемого скрипта, но до любых других I/O-событий или таймеров. К микрозадачам относятся разрешения промисов и другие асинхронные операции, которые приоритетнее обычных задач.
queueMicrotask(() => {
console.log('Microtask is executed');
});
console.log('Synchronous task is executed');В примере выше «Microtask is executed» будет залогировано после «Synchronous task is executed», но до любых I/O-операций вроде таймеров.
process.nextTick(): voidprocess.nextTick() используется для планирования колбэка, который выполнится сразу после завершения текущей операции. Это полезно в ситуациях, когда нужно гарантировать выполнение колбэка как можно скорее, но всё же после текущего контекста выполнения.
process.nextTick(() => {
console.log('Next tick callback');
});
console.log('Synchronous task executed');setImmediate(): voidsetImmediate() планирует выполнение колбэка в фазе check event loop Node.js, которая идёт после фазы poll, где обрабатывается большинство I/O-колбэков.
setImmediate(() => {
console.log('Immediate callback');
});
console.log('Synchronous task executed');- Используйте
queueMicrotask()для задач, которые должны выполниться сразу после текущего скрипта и до любых I/O- или таймерных колбэков, — как правило, для разрешений промисов. - Используйте
process.nextTick()для задач, которые должны выполниться до любых I/O-событий, — часто полезно для отсрочки операций или синхронной обработки ошибок. - Используйте
setImmediate()для задач, которые должны выполниться после фазы poll, когда обработано большинство I/O-колбэков.
Поскольку эти задачи выполняются вне текущего синхронного потока, неперехваченные исключения внутри этих колбэков не будут пойманы окружающими блоками try/catch и могут уронить приложение, если ими не управлять как следует (например, прикрепляя .catch() к промисам или используя глобальные обработчики ошибок вроде process.on('uncaughtException')).
Больше информации об event loop и порядке выполнения различных фаз см. в связанной статье Event Loop в Node.js.