Методы асинхронной обработки в 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-кода.