Асинхронное программирование в JavaScript: полное руководство для Node.js

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в асинхронное программирование в JavaScript

JavaScript — это однопоточный язык программирования с неблокирующей событийно-ориентированной моделью. В среде Node.js асинхронное программирование является не просто удобной опцией, а фундаментальной необходимостью. Без асинхронности серверное приложение блокировалось бы при каждом запросе к базе данных, чтении файла или сетевом вызове, делая его непригодным для реальной работы.

В этом руководстве мы разберем три основные парадигмы асинхронного программирования в JavaScript: Callback, Promise и async/await. Вы узнаете, как работает Event Loop, научитесь обрабатывать ошибки и писать читаемый, производительный код для Node.js.

1. Callback: основа асинхронности

Callback (функция обратного вызова) — это функция, передаваемая в другую функцию в качестве аргумента и вызываемая после завершения асинхронной операции. Это самая старая и базовая модель асинхронности в JavaScript.

Простой пример с Callback

const fs = require('fs');

// Асинхронное чтение файла с callback
fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('Ошибка чтения файла:', err);
    return;
  }
  console.log('Содержимое файла:', data);
});

console.log('Чтение файла запущено...');
// Вывод: "Чтение файла запущено..." появится раньше содержимого файла

В этом примере callback-функция выполнится только после того, как файл будет полностью прочитан. Тем временем основной поток продолжает выполнение — это и есть неблокирующее поведение.

Callback Hell (Ад обратных вызовов)

Главный недостаток callback-ов — быстрая потеря читаемости при вложенных вызовах:

// Ад обратных вызовов
getUser(1, (err, user) => {
  if (err) throw err;
  getPosts(user.id, (err, posts) => {
    if (err) throw err;
    getComments(posts[0].id, (err, comments) => {
      if (err) throw err;
      console.log(user.name, posts[0].title, comments);
    });
  });
});

Такой код сложно отлаживать, поддерживать и расширять. К счастью, современный JavaScript предлагает гораздо более элегантные решения.

2. Promise: обещание результата

Promise (промис) — это объект, представляющий результат асинхронной операции, которая может быть завершена (fulfilled), отклонена (rejected) или находиться в ожидании (pending). Промисы позволяют избежать вложенности и строить цепочки вызовов.

Создание и использование Promise

// Функция, возвращающая Promise
function readFilePromise(filename) {
  return new Promise((resolve, reject) => {
    const fs = require('fs');
    fs.readFile(filename, 'utf8', (err, data) => {
      if (err) {
        reject(err); // Ошибка — промис отклонен
      } else {
        resolve(data); // Успех — промис выполнен
      }
    });
  });
}

// Использование промиса
readFilePromise('example.txt')
  .then(data => {
    console.log('Содержимое:', data);
    return data.length;
  })
  .then(length => {
    console.log('Длина текста:', length);
  })
  .catch(err => {
    console.error('Ошибка:', err);
  });

Методы Promise.all и Promise.race

Promise.all позволяет дождаться выполнения всех промисов, а Promise.race — первого завершенного:

const p1 = readFilePromise('file1.txt');
const p2 = readFilePromise('file2.txt');
const p3 = readFilePromise('file3.txt');

// Ждем все три файла
Promise.all([p1, p2, p3])
  .then(([data1, data2, data3]) => {
    console.log('Все файлы загружены');
    console.log(data1, data2, data3);
  })
  .catch(err => console.error('Ошибка в одном из файлов:', err));

// Ждем первый завершенный
Promise.race([p1, p2, p3])
  .then(firstResult => {
    console.log('Первый файл загружен:', firstResult);
  });

3. Async/Await: синхронный стиль для асинхронного кода

Ключевые слова async и await, появившиеся в ES2017 (ES8), позволяют писать асинхронный код так, как будто он синхронный. await можно использовать только внутри функций, объявленных с async.

Базовый синтаксис

const fs = require('fs').promises; // Версия промисов

async function readFiles() {
  try {
    const data1 = await fs.readFile('file1.txt', 'utf8');
    console.log('Первый файл:', data1);
    
    const data2 = await fs.readFile('file2.txt', 'utf8');
    console.log('Второй файл:', data2);
    
    return { data1, data2 };
  } catch (error) {
    console.error('Ошибка при чтении файлов:', error);
    throw error; // Пробрасываем ошибку дальше
  }
}

// Вызов async функции
readFiles()
  .then(result => console.log('Результат:', result))
  .catch(err => console.error('Финальная ошибка:', err));

Параллельное выполнение с async/await

Используйте Promise.all вместе с await для параллельного выполнения независимых задач:

async function loadUserProfile(userId) {
  try {
    // Параллельный запуск независимых запросов
    const [user, posts, settings] = await Promise.all([
      fetchUser(userId),
      fetchUserPosts(userId),
      fetchUserSettings(userId)
    ]);
    
    return {
      name: user.name,
      postsCount: posts.length,
      theme: settings.theme
    };
  } catch (error) {
    console.error('Не удалось загрузить профиль:', error);
    throw new Error('ProfileLoadError');
  }
}

4. Event Loop и обработка ошибок

Как работает Event Loop

Event Loop (цикл событий) — это механизм, который позволяет Node.js выполнять неблокирующие операции, несмотря на однопоточность. Он работает по принципу:

  1. Выполняется синхронный код (стек вызовов)
  2. Асинхронные операции (setTimeout, I/O, промисы) регистрируются в очередях
  3. Когда стек освобождается, Event Loop берет задачи из очередей (микрозадач и макрозадач)
  4. Микрозадачи (промисы, process.nextTick) имеют приоритет над макрозадачами (setTimeout, I/O)
console.log('1. С                

Блоги

Рекомендаций по книгам