Async/await паттерны

Асинхронное программирование в JavaScript является критически важным для работы с сетевыми запросами, таймерами, файловой системой и другими операциями, которые могут занимать продолжительное время. Паттерн async/await предоставляет синтаксический сахар поверх промисов, делая асинхронный код более читаемым и управляемым.


Ключевые принципы async и await

  • async — модификатор функции, который позволяет использовать внутри неё await. Любая функция, объявленная как async, автоматически возвращает промис. Даже если функция возвращает примитивное значение, оно оборачивается в Promise.resolve().
async function fetchData() {
    return 42; // эквивалентно Promise.resolve(42)
}
  • await — оператор, который приостанавливает выполнение функции до завершения промиса. Результат выражения с await — значение, которое промис резолвит, либо выбрасывается ошибка, если промис отклонён.
async function getData() {
    const result = await fetch('https://api.example.com/data');
    const json = await result.json();
    return json;
}

Последовательное и параллельное выполнение

Последовательное выполнение: Когда несколько асинхронных операций зависят друг от друга, используют await в цепочке.

async function processSequential() {
    const step1 = await fetchStep1();
    const step2 = await fetchStep2(step1);
    return step2;
}

Параллельное выполнение: Если операции независимы, их лучше запускать одновременно с помощью Promise.all, чтобы ускорить выполнение.

async function processParallel() {
    const [data1, data2] = await Promise.all([fetchData1(), fetchData2()]);
    return data1 + data2;
}

Обработка ошибок

Ошибки в async/await ловятся с использованием блока try/catch.

async function fetchWithErrorHandling() {
    try {
        const response = await fetch('https://api.example.com/data');
        if (!response.ok) throw new Error('Network response was not ok');
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Ошибка при загрузке данных:', error);
        throw error; // повторное выбрасывание ошибки
    }
}

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

async function fetchMultiple() {
    const results = await Promise.allSettled([fetchData1(), fetchData2()]);
    results.forEach((result) => {
        if (result.status === 'fulfilled') {
            console.log('Успешно:', result.value);
        } else {
            console.log('Ошибка:', result.reason);
        }
    });
}

Работа с циклами и async/await

Использование forEach с await может приводить к неожиданным результатам, так как forEach не поддерживает асинхронные функции корректно. Вместо этого применяют for...of или map с Promise.all.

const urls = ['url1', 'url2', 'url3'];

// Неправильно:
urls.forEach(async (url) => {
    const data = await fetch(url);
    console.log(await data.json());
});

// Правильно:
for (const url of urls) {
    const response = await fetch(url);
    const data = await response.json();
    console.log(data);
}

// Или параллельно:
await Promise.all(urls.map(async (url) => {
    const response = await fetch(url);
    const data = await response.json();
    console.log(data);
}));

Комбинирование с другими паттернами

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

function measureAsync(fn) {
    return async function(...args) {
        const start = Date.now();
        const result = await fn(...args);
        console.log(`Время выполнения: ${Date.now() - start} мс`);
        return result;
    };
}

const fetchDataMeasured = measureAsync(fetchData);
await fetchDataMeasured();

Ловушки и подводные камни

  1. Забытый await — функция вернёт промис, а не значение, что может нарушить логику.
  2. Блокировка UI — слишком длинные синхронные операции внутри async блоков могут замедлить интерфейс, хотя сам await не блокирует поток.
  3. Цепочки промисов — неправильное смешение then/catch и await иногда приводит к двойной обработке ошибок.
  4. Параллельные гонки — использование нескольких await без Promise.all может снижать производительность.

Практические рекомендации

  • Использовать await только внутри async функций.
  • Параллельные операции запускать через Promise.all для повышения скорости.
  • Всегда оборачивать критические операции в try/catch или использовать Promise.allSettled.
  • Предпочитать for...of для асинхронных циклов над forEach.
  • Для повторного использования логики асинхронных операций создавать обёртки и функции высшего порядка.

Примеры интеграции с Lighthouse

В работе с Lighthouse асинхронные функции широко применяются для:

  • Запуска анализа страниц через launchChromeAndRunLighthouse.
  • Получения отчётов JSON.
  • Асинхронного сохранения результатов на диск или базу данных.
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');

async function runLighthouse(url, opts = {}) {
    const chrome = await chromeLauncher.launch({chromeFlags: ['--headless']});
    opts.port = chrome.port;

    try {
        const results = await lighthouse(url, opts);
        console.log(results.lhr.categories.performance.score);
    } finally {
        await chrome.kill();
    }
}

Использование async/await позволяет строить чистую, читаемую цепочку запуска анализа, обработки результатов и корректного завершения Chrome, что особенно важно при автоматизации тестов производительности и SEO-аудитов.