JavaScriptMiddleTechnical
Что такое Promise? Чем они отличаются от callback'ов?
Promise — объект для асинхронного результата с тремя состояниями (pending/fulfilled/rejected). В отличие от callback, даёт плоские цепочки .then(), единый .catch() и удобную композицию через Promise.all/race/any.
Promise в JavaScript
Promise — объект, представляющий результат асинхронной операции, которая ещё не завершилась. Он может находиться в одном из трёх состояний:
- pending — операция выполняется
- fulfilled — операция завершилась успешно, есть значение
- rejected — операция завершилась с ошибкой, есть причина
Переход из pending в fulfilled или rejected необратим.
Callback: исходная модель
До появления Promise (ES2015) асинхронность строилась на колбэках — функциях, передаваемых в качестве аргументов и вызываемых по завершении операции. Узнаваемый паттерн Node.js — error-first callback:
fs.readFile('data.json', 'utf8', (err, data) => {
if (err) {
console.error('Ошибка:', err);
return;
}
JSON.parse(data, (parseErr, obj) => { // callback hell начинается здесь
if (parseErr) {
console.error('JSON error:', parseErr);
return;
}
saveToDb(obj, (dbErr, result) => {
if (dbErr) { /* ещё уровень */ }
// ...
});
});
});
Promise: решение проблем callback
// Тот же поток с промисами
fs.promises.readFile('data.json', 'utf8')
.then(data => JSON.parse(data))
.then(obj => saveToDb(obj))
.then(result => console.log('Saved:', result))
.catch(err => console.error('Ошибка:', err)); // одна точка обработки ошибок
async/await — синтаксический сахар над Promise
async function processFile() {
try {
const data = await fs.promises.readFile('data.json', 'utf8');
const obj = JSON.parse(data);
const result = await saveToDb(obj);
console.log('Saved:', result);
} catch (err) {
console.error('Ошибка:', err);
}
}
Создание Promise вручную
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function fetchWithRetry(url, retries = 3) {
return new Promise(async (resolve, reject) => {
for (let i = 0; i < retries; i++) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return resolve(await res.json());
} catch (err) {
if (i === retries - 1) reject(err);
else await delay(1000 * 2 ** i); // экспоненциальный backoff
}
}
});
}
Ключевые отличия от callback
- Единая обработка ошибок — один
.catch()покрывает всю цепочку, вместо проверкиif (err)на каждом уровне. - Компонуемость — Promise.all, race, any, allSettled для параллельного выполнения; callback-и требуют ручного счётчика.
- Нет «callback hell» — цепочка
.then()линейна; глубокая вложенность заменяется плоским кодом. - Гарантированный async — обработчик
.then()всегда вызывается асинхронно (как микрозадача), даже если промис уже fulfilled; callback может быть вызван синхронно. - Состояние хранится — к fulfilled промису можно подключить
.then()в любой момент и получить значение; callback вызывается ровно один раз и значение теряется.
Promisification — обёртка над callback API
const { promisify } = require('util');
const readFile = promisify(require('fs').readFile);
// Теперь readFile возвращает Promise
const content = await readFile('file.txt', 'utf8');
// Вручную
function promisifyCallback(fn) {
return (...args) => new Promise((resolve, reject) => {
fn(...args, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
}
Подводные камни
- Проглоченные ошибки — Promise без
.catch()тихо поглощает исключения; в Node.js ≥15 это завершает процесс сunhandledRejection. - new Promise(async executor) — async-функция в конструкторе Promise опасна: брошенное исключение внутри не попадёт в reject промиса, а уйдёт в unhandledRejection.
- return в .then() — забытый return прерывает цепочку: следующий
.then()получит undefined вместо нужного значения. - Смешивание async/await и .then() — технически корректно, но создаёт путаницу; выбирайте один стиль в рамках функции.
- Promise не отменяемый — нет встроенной отмены; используйте AbortController + AbortSignal для fetch и других отменяемых операций.
- await в цикле —
for...ofс await выполняет итерации последовательно; для параллельного выполнения нуженPromise.all(items.map(...)). - Ошибки после resolve/reject — если в executor выбросить исключение после вызова resolve, оно проигнорируется; Promise уже settled.
Common mistakes
- Смешивать «Promise» с похожим механизмом без критерия выбора.
- Игнорировать риск: неверно оценить границы применения темы «Promise» и получить хрупкое решение.
- Показывать только синтаксис и не объяснять поведение в runtime или сборке.
What the interviewer is testing
- Объясняет композиция асинхронного результата и отличие от callback-цепочек.
- Показывает на примере, как работает: Promise представляет будущий результат в состояниях pending, fulfilled или rejected и позволяет строить цепочки с единым каналом ошибок вместо ручной передачи callback-функций.
- Называет production-нюанс и граничный случай для темы «Promise».