Асинхронщина в Node.js: От колбэков к async/await

Вступление

Node.js — крут во многом благодаря своей способности не тормозить выполнение, работая с задачами асинхронно. Но как разобраться с этой асинхронностью? Если в примере с сервером мы обошлись простым колбэком, то в реальной жизни обычно хочется чего-то помощнее. Щас посмотрим, как развивался асинхронный код: от колбэчного ада до классных промисов и удобного async/await.

 

Проблема ад колбэков:

// Классическая проблема вложенных колбэков
readFile('file1.txt', (err, data1) => {
    if (err) throw err;
    readFile('file2.txt', (err, data2) => {
        if (err) throw err;
        writeFile('result.txt', data1 + data2, (err) => {
            if (err) throw err;
            console.log('Готово!');
        });
    });
});

Решение 1: Промисы (Promises)

 

Промисы – это штуки, которые показывают, чем закончилась асинхронная операция (успехом или провалом).

Что важно знать:

Состояния: pending (в процессе), fulfilled (готово), rejected (отклонено)

Методы: .then() – для обработки успехов, .catch() – для ловли ошибок

Цепочки: позволяют выполнять асинхронные штуки по очереди

Пример: Читаем файл с помощью промисов.

// Предположим, что readFilePromise возвращает промис
const fs = require('fs').promises;

fs.readFile('config.json', 'utf8')
    .then(data => {
        console.log('Файл прочитан успешно');
        const config = JSON.parse(data);
        return config; // Передаем в следующий then
    })
    .then(config => {
        console.log('Имя приложения:', config.appName);
        return fs.writeFile('output.log', `Конфиг загружен: ${new Date()}`);
    })
    .then(() => {
        console.log('Лог записан');
    })
    .catch(error => {
        console.error('Произошла ошибка:', error.message);
    });

Решение 2: Async/Await — современный подход

Синтаксис async/await делает асинхронный код похожим на синхронный, что упрощает его чтение и поддержку.

Ключевые моменты:

  • async функция всегда возвращает промис

  • await можно использовать только внутри async функций

  • Код выполняется «сверху вниз», что интуитивно понятнее

Пример: Практическое использование async/await

const fs = require('fs').promises;
const axios = require('axios'); // популярная библиотека для HTTP-запросов

async function fetchUserData(userId) {
    try {
        // 1. Читаем локальный файл с настройками
        const settings = await fs.readFile('settings.json', 'utf8');
        const { apiUrl } = JSON.parse(settings);
        
        // 2. Делаем HTTP-запрос к API
        console.log('Загружаем данные пользователя...');
        const response = await axios.get(`${apiUrl}/users/${userId}`);
        
        // 3. Обрабатываем полученные данные
        const user = response.data;
        user.lastAccess = new Date();
        
        // 4. Сохраняем обновленные данные
        await fs.writeFile(
            `user-${userId}.json`,
            JSON.stringify(user, null, 2)
        );
        
        console.log(`Данные пользователя ${user.name} сохранены!`);
        return user;
        
    } catch (error) {
        console.error('Ошибка в процессе загрузки:', error.message);
        // Можно вернуть значение по умолчанию или пробросить ошибку дальше
        throw error;
    }
}

// Использование функции
async function main() {
    try {
        const user = await fetchUserData(123);
        console.log('Работа завершена успешно');
    } catch (error) {
        console.log('Не удалось выполнить операцию');
    }
}

main();

Полезные паттерны

Параллельное выполнение:

async function loadMultipleResources() {
    // Запускаем все промисы параллельно
    const [users, posts, settings] = await Promise.all([
        fetch('/api/users'),
        fetch('/api/posts'),
        fetch('/api/settings')
    ]);
    
    // Все данные загружены параллельно!
    return { users, posts, settings };
}

Обработка нескольких попыток:

async function fetchWithRetry(url, retries = 3) {
    for (let i = 0; i < retries; i++) {
        try {
            const response = await fetch(url);
            return await response.json();
        } catch (error) {
            if (i === retries - 1) throw error;
            console.log(`Попытка ${i + 1} не удалась, повторяем...`);
            await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
        }
    }
}

Итог

Асинхронное программирование – это основа Node.js. Разберитесь с промисами, а потом переходите к async/await, чтобы писать понятный и простой в поддержке код. Для начала:

  • Колбэки → для простых случаев и если нужна совместимость
  • Промисы → для сложных цепочек асинхронных операций
  • Async/Await → для современного и читаемого кода

Попрактикуйтесь на простых примерах, и очень скоро асинхронность станет вашим козырем в Node.js разработке!