JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Node.Js и async/await: синтаксис, преимущества

Node.Js и async/await: синтаксис, преимущества

Модуль 4: Node.js, Next.js и Angular
2 уровень , 5 лекция
Открыта

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, он:

  1. Останавливает выполнение текущей async-функции.
  2. Ждёт, пока промис справа от await завершится (fulfilled или rejected).
  3. Если промис fulfilled (успех) — результат присваивается переменной.
  4. Если промис 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 (но всё равно в виде промиса).

1
Задача
Модуль 4: Node.js, Next.js и Angular, 2 уровень, 5 лекция
Недоступна
Объявление async-функции и возврат значения
Объявление async-функции и возврат значения
1
Задача
Модуль 4: Node.js, Next.js и Angular, 2 уровень, 5 лекция
Недоступна
Использование await для работы с промисом
Использование await для работы с промисом
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ