1. Почему async/await? Проблемы с промисами
Давайте вспомним, как выглядел код с промисами:
fetchUser()
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.then(comments => {
console.log('Комментарии:', comments);
})
.catch(err => {
console.error('Произошла ошибка:', err);
});
Вроде бы неплохо, но если появляется много асинхронных шагов, особенно с условиями, try/catch и логикой, код начинает превращаться в «лес» из then и catch. А если вложить ещё промисы друг в друга — получится callback hell, только с then.
async/await позволяет писать асинхронный код почти так же, как синхронный — без цепочек then, без вложенности, с привычным try/catch. Даже если вы уже изучали async/await, мы сейчас его кратенько повторим, чтобы новый материал пошел как по маслу :P
2. async/await: базовый синтаксис
async-функция
Чтобы использовать await, нужно объявить функцию с ключевым словом async:
async function myFunction() {
// тут можно использовать await
}
Что делает async?
- Любая функция, объявленная с async, всегда возвращает Promise.
- Даже если вы внутри возвращаете обычное значение, оно оборачивается в Promise.
async function foo() {
return 42;
}
foo().then(result => console.log(result)); // выведет 42
await
Ключевое слово await можно использовать только внутри async-функции. Оно «останавливает» выполнение функции до тех пор, пока промис справа от await не завершится.
async function getData() {
const user = await fetchUser();
const posts = await fetchPosts(user.id);
console.log(posts);
}
Важно! При этом весь остальной код программы не блокируется — только «текущая» async-функция ждёт результата.
Пример для разогрева
Давайте сравним подходы на одном и том же примере.
С промисамиfunction delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
delay(1000)
.then(() => {
console.log('Прошла 1 секунда');
return delay(1000);
})
.then(() => {
console.log('Прошла ещё 1 секунда');
});
С async/await
async function run() {
await delay(1000);
console.log('Прошла 1 секунда');
await delay(1000);
console.log('Прошла ещё 1 секунда');
}
run();
Видите, насколько проще и понятнее выглядит код с async/await? Нет лишних then, вложенностей и цепочек — просто пишем шаг за шагом.
3. Как работает await: под капотом
Когда интерпретатор встречает await, он:
- Останавливает выполнение текущей async-функции.
- Ждёт, пока промис справа от await завершится (fulfilled или rejected).
- Если промис fulfilled (успех) — результат присваивается переменной.
- Если промис rejected (ошибка) — выбрасывается исключение (throw), которое можно поймать через try/catch.
Всё остальное приложение продолжает работать! Это не блокирует основной поток, не зависает браузер или Node.js.
4. Пример: интеграция в наше приложение
Допустим, мы делаем простое приложение для работы с задачами (to-do list) на Node.js. У нас есть функции для чтения и записи задач в файл с использованием промисов (например, через fs.promises).
С промисамиconst fs = require('fs/promises');
function addTask(task) {
return fs.readFile('tasks.json', 'utf-8')
.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);
});
}
С async/await
const fs = require('fs/promises');
async function addTask(task) {
try {
const data = await fs.readFile('tasks.json', 'utf-8');
const tasks = JSON.parse(data);
tasks.push(task);
await fs.writeFile('tasks.json', JSON.stringify(tasks, null, 2));
console.log('Задача добавлена!');
} catch (err) {
console.error('Ошибка при добавлении задачи:', err);
}
}
Код стал короче, логика — линейной, а обработка ошибок — привычной через try/catch.
5. Обработка ошибок: try/catch с async/await
Одна из главных фишек async/await — возможность использовать обычный try/catch для асинхронных ошибок. Это делает обработку ошибок гораздо более привычной и понятной.
Примерasync function fetchData() {
try {
const user = await fetchUser();
const posts = await fetchPosts(user.id);
return posts;
} catch (error) {
console.error('Ошибка при получении данных:', error.message);
// Можно вернуть дефолтное значение или пробросить ошибку дальше
return [];
}
}
Аналогичный код с промисами выглядел бы так:
fetchUser()
.then(user => fetchPosts(user.id))
.catch(error => {
console.error('Ошибка при получении данных:', error.message);
return [];
});
Но если таких шагов много, вложенность catch/then быстро становится неудобной.
6. async/await и параллелизм
await отлично подходит для последовательных операций, но иногда нужно делать несколько запросов параллельно (например, загрузить сразу несколько файлов или данных). Если просто написать:
const a = await fetchA();
const b = await fetchB();
— то fetchA выполнится, потом — fetchB. Это не параллельно!
Как запустить параллельно?
Используйте Promise.all:
async function getAllData() {
const [a, b] = await Promise.all([fetchA(), fetchB()]);
console.log('Данные:', a, b);
}
Важно! Promise.all ждёт, пока завершатся все промисы, и только потом продолжает выполнение.
7. Возвращаемое значение из async-функции
Любая async-функция всегда возвращает Promise. Даже если вы явно возвращаете простое значение:
async function foo() {
return 42;
}
const promise = foo(); // promise — это Promise, а не число!
promise.then(value => console.log(value)); // выведет 42
Если в async-функции выбросить ошибку (throw), Promise перейдёт в состояние rejected:
async function fail() {
throw new Error('Ошибка!');
}
fail().catch(err => console.log('Поймали ошибку:', err.message));
8. Использование await вне async-функции
В старых версиях Node.js и браузеров await можно было использовать только внутри async-функции. Сейчас (Node.js 14+ и современные браузеры) await можно использовать и на верхнем уровне модулей, если файл запускается как ES-модуль (с расширением .mjs или с "type": "module" в package.json):
// файл: script.mjs
const data = await fetchData();
console.log(data);
Если вы используете CommonJS (require/module.exports), await вне async-функции вызовет ошибку.
9. async/await и обработка ошибок в нескольких местах
Можно использовать несколько try/catch для разных шагов:
async function process() {
let user;
try {
user = await fetchUser();
} catch (e) {
console.error('Ошибка при получении пользователя:', e);
return;
}
try {
const posts = await fetchPosts(user.id);
console.log(posts);
} catch (e) {
console.error('Ошибка при получении постов:', e);
}
}
Это удобно, если разные шаги требуют разной логики обработки ошибок.
10. Интеграция с существующим кодом на промисах
await работает с любым объектом, у которого есть метод then (promise-like). Это значит, что можно постепенно переводить старый код на async/await, не переписывая всё сразу.
// Старый код:
function fetchUser() {
return Promise.resolve({ id: 1, name: 'Alice' });
}
// Новый код:
async function main() {
const user = await fetchUser(); // работает!
console.log(user);
}
11. Типичные ошибки при использовании async/await
Ошибка №1: забыли await.
Если вы забыли написать await перед вызовом промиса, переменная получит сам промис, а не результат. Например:
async function foo() {
const data = fetchData(); // забыли await!
console.log(data); // Promise { ... }, а не данные
}
Ошибка №2: await вне async-функции.
В обычных скриптах (не модулях) нельзя использовать await вне async-функции — получите синтаксическую ошибку.
Ошибка №3: забыли обработать rejected-промис.
Если внутри async-функции промис завершился с ошибкой, а вы не обернули код в try/catch — ошибка «вывалится» наружу и может привести к падению программы.
Ошибка №4: последовательное выполнение вместо параллельного.
Если несколько await идут подряд, операции выполняются друг за другом, а не параллельно. Для параллельного выполнения используйте Promise.all.
Ошибка №5: забыли вернуть значение из async-функции.
Если забыли return, функция вернёт undefined (но всё равно в виде промиса).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ