1. Проблема коллбеков и зачем нужны Promises
Давайте вспомним, как выглядел типичный асинхронный код на коллбеках:
const fs = require('fs');
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error('Ошибка чтения файла:', err);
return;
}
console.log('Содержимое файла:', data);
});
Всё хорошо, пока у нас одна операция. Но стоит добавить вторую, третью, сделать обработку ошибок — и вот уже ваш код напоминает рождественскую гирлянду из вложенных функций.
Callback hell — это когда уровень вложенности и количество кода растёт экспоненциально, а отладка становится похожа на разгадывание кроссворда без подсказок.
Promises пришли на смену коллбекам, чтобы сделать асинхронный код более читаемым, предсказуемым и удобным для обработки ошибок.
Что такое Promise
Promise (обещание) — это специальный объект, который представляет результат асинхронной операции, которая может завершиться успешно (fulfilled) или с ошибкой (rejected).
Представьте, что вы заказали пиццу. Вам обещают привезти её через 30 минут. Вы не знаете, привезут ли её вовремя, но у вас есть обещание (promise). Когда курьер приедет, вы либо получите пиццу (успех), либо останетесь голодным (ошибка).
Состояния Promise
У Promise есть три состояния:
| Состояние | Описание |
|---|---|
| pending | Ожидание (обещание ещё не выполнено) |
| fulfilled | Выполнено успешно (обещание сдержано) |
| rejected | Ошибка (обещание не сдержано) |
2. Создание Promise
Синтаксис создания Promise очень прост:
const myPromise = new Promise((resolve, reject) => {
// Здесь выполняется асинхронная операция
// Если всё хорошо:
// resolve(результат);
// Если что-то пошло не так:
// reject(ошибка);
});
Пример: Promise, который выполняется успешно
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Готово!'); // Через 1 секунду Promise выполнится успешно
}, 1000);
});
console.log('Обещание создано!');
Пример: Promise с ошибкой
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
reject('Что-то пошло не так');
}, 1000);
});
3. then: обработка успешного результата
Чтобы получить результат выполнения Promise, используется метод then:
promise.then((result) => {
console.log('Результат:', result);
});
then принимает функцию, которая будет вызвана, когда Promise выполнится успешно (fulfilled).
Пример:
const promise = new Promise((resolve) => {
setTimeout(() => resolve('Пицца доставлена!'), 1500);
});
promise.then((result) => {
console.log(result); // "Пицца доставлена!"
});
4. catch: обработка ошибок
Для обработки ошибок используется метод catch. Он срабатывает, если Promise был отклонён (rejected):
promise.catch((error) => {
console.error('Ошибка:', error);
});
Пример:
const promise = new Promise((resolve, reject) => {
setTimeout(() => reject('Пиццу не привезли :('), 1500);
});
promise
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(error); // "Пиццу не привезли :("
});
5. finally: всегда выполняется
Иногда нужно выполнить какое-то действие всегда — независимо от того, успешно завершился Promise или с ошибкой (например, убрать спиннер загрузки). Для этого есть метод finally:
promise
.then((result) => {
console.log('Успех:', result);
})
.catch((error) => {
console.error('Ошибка:', error);
})
.finally(() => {
console.log('Завершено!'); // Всегда выполняется
});
6. Цепочки Promises: магия then
Одна из главных фишек Promises — возможность строить цепочки из then:
new Promise((resolve) => {
setTimeout(() => resolve(2), 1000);
})
.then((num) => {
console.log('Первый then:', num); // 2
return num * 2;
})
.then((num) => {
console.log('Второй then:', num); // 4
return num * 2;
})
.then((num) => {
console.log('Третий then:', num); // 8
});
Каждый then возвращает новый Promise, и результат передаётся дальше по цепочке.
Пример с ошибкой в цепочке
Если в любом then произойдёт ошибка (например, выбросится исключение или вернётся rejected Promise), управление перейдёт в ближайший catch:
new Promise((resolve) => {
resolve('Начало');
})
.then((msg) => {
throw new Error('Ошибка в then!');
})
.catch((err) => {
console.error('Поймали ошибку:', err.message);
});
7. Практика: Promise для чтения файла
Давайте попробуем использовать Promise для асинхронного чтения файла с помощью модуля fs.promises:
const fs = require('fs/promises');
fs.readFile('file.txt', 'utf8')
.then((data) => {
console.log('Содержимое файла:', data);
})
.catch((err) => {
console.error('Ошибка чтения файла:', err);
})
.finally(() => {
console.log('Операция завершена');
});
Теперь вы видите, насколько проще и чище выглядит код по сравнению с callback-стилем. Ошибки и успехи обрабатываются отдельно, вложенности нет, а цепочки then позволяют элегантно строить последовательные асинхронные операции.
8. Развиваем наше приложение: асинхронная цепочка
Допустим, мы делаем мини-приложение, которое:
- Читает файл с задачами (tasks.json)
- Добавляет новую задачу
- Записывает результат обратно в файл
Вот как это может выглядеть с Promises:
const fs = require('fs/promises');
function addTask(task) {
return fs.readFile('tasks.json', 'utf8')
.then((data) => {
const tasks = JSON.parse(data);
tasks.push(task);
return fs.writeFile('tasks.json', JSON.stringify(tasks, null, 2));
})
.then(() => {
console.log('Задача добавлена!');
})
.catch((err) => {
console.error('Ошибка:', err);
});
}
addTask({ title: 'Изучить Promises', done: false });
Здесь мы используем цепочку then: сначала читаем файл, затем парсим JSON, добавляем задачу, записываем обратно — и всё это без коллбекового ада!
9. Типичные ошибки при работе с Promises
Ошибка №1: забыли вернуть Promise из then
Если внутри then вы делаете асинхронную операцию, обязательно возвращайте Promise! Иначе цепочка не дождётся её завершения.
// Неправильно
.then(() => {
fs.writeFile('file.txt', 'Текст'); // забыли return!
})
// Правильно
.then(() => {
return fs.writeFile('file.txt', 'Текст');
})
Ошибка №2: не обработали ошибку
Если не добавить catch, ошибка может "потеряться", и приложение завершится с неожиданной ошибкой. Всегда добавляйте catch в конце цепочки.
Ошибка №3: попытка использовать результат вне then
Promise — это асинхронность! Всё, что зависит от результата, должно быть внутри then/catch, иначе получите undefined.
let data;
fs.readFile('file.txt', 'utf8').then((result) => {
data = result;
});
console.log(data); // undefined, потому что Promise ещё не завершён!
Ошибка №4: забыли finally для очистки ресурсов
Если у вас есть ресурсы, которые нужно обязательно закрыть/освободить, используйте finally — иначе при ошибках cleanup не произойдёт.
Ошибка №5: вложенность then вместо цепочки
Не пишите так:
fs.readFile('file.txt', 'utf8').then((data) => {
fs.writeFile('copy.txt', data).then(() => {
console.log('Готово');
});
});
Лучше строить цепочку:
fs.readFile('file.txt', 'utf8')
.then((data) => fs.writeFile('copy.txt', data))
.then(() => console.log('Готово'));
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ