Методы асинхронной обработки в Node.js: от колбэков до современного синтаксиса

Раздел: Node.js -> Асинхронное программирование в Node.js

Введение в асинхронность Node.js

Node.js использует однопоточную событийную модель, основанную на цикле событий (event loop). Асинхронные операции (I/O, сетевые запросы, чтение файлов) не блокируют основной поток. Для работы с ними применяются колбэки, промисы и async/await.

Как сделать асинхронный код читаемым и управляемым?

Современный стандарт - async/await. Он позволяет писать асинхронный код так, будто он синхронный, сохраняя неблокирующий характер.

const fs = require('fs/promises');

async function readConfig() {
  try {
    const data = await fs.readFile('config.json', 'utf8');
    const config = JSON.parse(data);
    console.log('Config:', config);
  } catch (err) {
    console.error('Ошибка чтения:', err.message);
  }
}

readConfig();

Node js async (асинхронный node.js)

Вывод при наличии config.json:
Config: { port: 3000, host: 'localhost' }

Цель: упрощение асинхронного потока, возможность использовать try/catch для обработки ошибок. Случаи использования: любой асинхронный код, где нужна последовательность операций, например чтение файлов, запросы к API, работа с базой данных.

Типичные ошибки:

  • Забытый await - функция возвращает промис, а не результат.
  • Отсутствие try/catch - необработанные ошибки приводят к unhandled promise rejection.
  • Использование async для синхронных функций - избыточно.

Решение: всегда оборачивать код в try/catch, проверять, что асинхронная функция возвращает промис, использовать линтеры (например, eslint-plugin-promise).

Как выполнить асинхронную операцию без блокировки, используя колбэки?

Колбэки - традиционный способ в Node.js, где функция передаётся как аргумент и вызывается после завершения операции.

const fs = require('fs');

fs.readFile('config.json', 'utf8', (err, data) => {
  if (err) {
    console.error('Ошибка:', err.message);
    return;
  }
  try {
    const config = JSON.parse(data);
    console.log('Config:', config);
  } catch (parseErr) {
    console.error('Ошибка парсинга:', parseErr.message);
  }
});
Вывод аналогичен предыдущему примеру.

Цель: простое выполнение одной асинхронной операции. Случаи использования: простые задачи, обратная совместимость.

Типичные ошибки:

  • Callback hell (ад колбэков) - вложенные колбэки снижают читаемость.
  • Нарушение соглашения «ошибка первым» (error-first) - игнорирование первого аргумента.
  • Потеря контекста this при использовании в методах объектов.

Решение: избегать глубокой вложенности, использовать модульные функции или переходить на промисы.

Как организовать асинхронный код без вложенности, используя промисы?

Промисы предоставляют цепочку .then() и .catch(), устраняя вложенность колбэков.

const fs = require('fs/promises');

fs.readFile('config.json', 'utf8')
  .then(data => JSON.parse(data))
  .then(config => console.log('Config:', config))
  .catch(err => console.error('Ошибка:', err.message));
Результат тот же.

Цель: линейная цепочка асинхронных операций, централизованная обработка ошибок. Случаи использования: последовательные зависимости (запрос A, затем B, затем C), параллельные операции через Promise.all.

Типичные ошибки:

  • Игнорирование возврата промиса из .then() - следующий .then() получит undefined.
  • Необработанные отклонения (unhandled rejection) - отсутствие .catch() в конце цепочки.
  • Параллельное выполнение вместо последовательного - забытый Promise.all.

Решение: всегда завершать цепочку .catch(), использовать Promise.all для параллельности, явно возвращать значения.

Заключение: выбор метода зависит от контекста. Для нового кода рекомендуется async/await. Промисы подходят для цепочек и параллелей. Колбэки остаются в legacy-коде. Понимание всех трёх подходов необходимо для работы с Node.js.

Расширенные примеры асинхронного кода в Node.js

Пример 1: Последовательное выполнение с async/await и обработка ошибок

Пример
const fs = require('fs/promises');
const path = require('path');

async function processFiles() {
  try {
    const dir = './data';
    const files = await fs.readdir(dir);
    for (const file of files) {
      const filePath = path.join(dir, file);
      const content = await fs.readFile(filePath, 'utf8');
      const processed = content.toUpperCase();
      await fs.writeFile(filePath, processed);
      console.log(`Обработан: ${file}`);
    }
    console.log('Все файлы обработаны');
  } catch (err) {
    console.error('Ошибка обработки:', err.message);
  }
}

processFiles();
Вывод:
Обработан: a.txt
Обработан: b.txt
Все файлы обработаны
(если каталог data существует и файлы есть)

Пример 2: Параллельное выполнение с Promise.all и обработка частичных ошибок

Пример
const fetch = require('node-fetch');

async function fetchMultiple(urls) {
  const promises = urls.map(url =>
    fetch(url)
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .catch(err => ({ error: err.message, url }))
  );
  const results = await Promise.all(promises);
  results.forEach((result, index) => {
    if (result.error) {
      console.log(`Ошибка для URL ${urls[index]}: ${result.error}`);
    } else {
      console.log(`Данные из ${urls[index]}:`, result);
    }
  });
}

fetchMultiple([
  'https://api.example.com/data',
  'https://invalid.url'
]);
Вывод:
Данные из https://api.example.com/data: { ... }
Ошибка для URL https://invalid.url: getaddrinfo ENOTFOUND invalid.url

Пример 3: Использование EventEmitter для асинхронных событий

Пример
const EventEmitter = require('events');

class AsyncProcessor extends EventEmitter {
  async process(data) {
    this.emit('start', data);
    try {
      const result = await this.simulateAsync(data);
      this.emit('success', result);
      return result;
    } catch (err) {
      this.emit('error', err);
      throw err;
    }
  }

  simulateAsync(value) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (value > 0) resolve(value * 2);
        else reject(new Error('Значение должно быть положительным'));
      }, 1000);
    });
  }
}

const processor = new AsyncProcessor();
processor.on('start', (d) => console.log('Начало обработки:', d));
processor.on('success', (r) => console.log('Успешно:', r));
processor.on('error', (e) => console.error('Ошибка:', e.message));

processor.process(5).catch(() => {});
processor.process(-1).catch(() => {});
Вывод (через 1 секунду):
Начало обработки: 5
Начало обработки: -1
Успешно: 10
Ошибка: Значение должно быть положительным

Пример 4: Преобразование колбэка в промис через promisify

Пример
const fs = require('fs');
const { promisify } = require('util');

const readFileAsync = promisify(fs.readFile);
const writeFileAsync = promisify(fs.writeFile);

async function copyFile(src, dest) {
  try {
    const data = await readFileAsync(src, 'utf8');
    await writeFileAsync(dest, data);
    console.log('Файл скопирован');
  } catch (err) {
    console.error('Ошибка копирования:', err.message);
  }
}

copyFile('source.txt', 'dest.txt');
Вывод:
Файл скопирован (если файл source.txt существует)

Эти примеры демонстрируют типичные паттерны использования асинхронности в Node.js: последовательную и параллельную обработку, событийную модель и адаптацию legacy-кода.

Асинхронный Node.js - comments

En
Node js async (javascript)