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

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

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

Начать курс

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

JavaScript — это однопоточный язык с неблокирующим событийным циклом (Event Loop). Это значит, что в один момент времени выполняется только одна операция, но благодаря асинхронности мы можем не ждать завершения медленных задач (чтение файла, запрос к API, таймер), а продолжать выполнение другого кода. В этой статье мы разберём все ключевые концепции асинхронного программирования в среде Node.js, от колбэков до современного синтаксиса async/await.

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

1. Callback-функции и проблема Callback Hell

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

Пример простого колбэка

const fs = require('fs');

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

console.log('Это сообщение появится раньше, чем содержимое файла');

Проблема возникает, когда нужно выполнить несколько последовательных асинхронных операций. Вложенность колбэков растёт, и код становится нечитаемым — это явление называют "Callback Hell" или "Ад колбэков".

Пример Callback Hell

fs.readFile('file1.txt', (err, data1) => {
    if (err) throw err;
    fs.readFile('file2.txt', (err, data2) => {
        if (err) throw err;
        fs.readFile('file3.txt', (err, data3) => {
            if (err) throw err;
            // Обработка всех трёх файлов
            console.log(data1 + data2 + data3);
        });
    });
});

Такой код трудно поддерживать, тестировать и отлаживать. Решение этой проблемы пришло с появлением Promise.

2. Promise: избавление от Callback Hell

Promise (обещание) — это объект, который представляет собой результат асинхронной операции: успешное выполнение (resolve) или ошибку (reject). Promise позволяет выстраивать цепочки вызовов, делая код более плоским и читаемым.

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

// Функция, возвращающая Promise
const readFilePromise = (filename) => {
    return new Promise((resolve, reject) => {
        fs.readFile(filename, 'utf8', (err, data) => {
            if (err) reject(err);
            else resolve(data);
        });
    });
};

// Использование
readFilePromise('file1.txt')
    .then(data => {
        console.log('Файл прочитан:', data);
        return readFilePromise('file2.txt');
    })
    .then(data => {
        console.log('Второй файл:', data);
    })
    .catch(err => {
        console.error('Ошибка:', err);
    });

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

Для параллельного выполнения нескольких асинхронных операций существуют полезные статические методы:

  • Promise.all() — ждёт выполнения всех промисов. Если хотя бы один завершится с ошибкой, весь Promise.all переходит в состояние rejected.
  • Promise.race() — возвращает результат первого завершённого промиса (успех или ошибка).
  • Promise.allSettled() — ждёт завершения всех промисов, возвращает результаты каждого (успех или ошибку).
const promise1 = readFilePromise('file1.txt');
const promise2 = readFilePromise('file2.txt');
const promise3 = readFilePromise('file3.txt');

Promise.all([promise1, promise2, promise3])
    .then(results => {
        console.log('Все файлы прочитаны:', results);
    })
    .catch(err => {
        console.error('Ошибка в одном из файлов:', err);
    });

3. Async/Await: синхронный код для асинхронных операций

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

Основы синтаксиса

Функция, объявленная с async, всегда возвращает Promise. Ключевое слово await можно использовать только внутри async-функции — оно приостанавливает выполнение функции до тех пор, пока Promise не разрешится.

const processFiles = async () => {
    try {
        const data1 = await readFilePromise('file1.txt');
        const data2 = await readFilePromise('file2.txt');
        const data3 = await readFilePromise('file3.txt');
        
        console.log('Все файлы прочитаны последовательно');
        return data1 + data2 + data3;
    } catch (error) {
        console.error('Произошла ошибка:', error);
        throw error; // Пробрасываем ошибку дальше
    }
};

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

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

Для запуска нескольких операций параллельно используйте Promise.all вместе с await:

const processFilesParallel = async () => {
    try {
        const [data1, data2, data3] = await Promise.all([
            readFilePromise('file1.txt'),
            readFilePromise('file2.txt'),
            readFilePromise('file3.txt')
        ]);
        
        console.log('Файлы прочитаны параллельно');
        return { data1, data2, data3 };
    } catch (error) {
        console.error('Ошибка при параллельном чтении:', error);
    }
};

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

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

Event Loop — это механизм, который постоянно проверяет, есть ли задачи для выполнения. Понимание его работы критически важно для написания эффективного асинхронного кода.

Основные фазы Event Loop в Node.js:

  1. Timers — выполняются колбэки setTimeout/setInterval.
  2. I/O callbacks — обработка завершённых операций ввода-вывода.
  3. Блоги

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