Если вы когда-нибудь писали код на JavaScript и замечали, что иногда он выполняется не в том порядке, в котором вы его написали — поздравляю, вы столкнулись с асинхронностью. Это не баг, а фича, но фича коварная. В этой статье я объясню, как работает асинхронный JavaScript, покажу на примерах, как с ним дружить, и дам пару лайфхаков, которые сэкономят вам часы нервов.

Digital-студия WNDER

Что такое асинхронность и почему она вообще нужна?

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

В JavaScript асинхронность нужна, когда мы делаем запросы к серверу, читаем файлы, работаем с таймерами и вообще с любыми операциями, которые могут занять время. Если бы всё было синхронно, браузер бы замирал на время выполнения запроса, и пользователь видел бы мёртвую страницу. А так — интерфейс продолжает жить, а результат приходит, когда готов.

Колбэки: дедушка асинхронности

Самый старый способ работы с асинхронностью — колбэки (callback). Это просто функция, которая вызывается после завершения асинхронной операции. Выглядит это так:

console.log('Начинаем');

setTimeout(() => {
  console.log('Прошло 2 секунды');
}, 2000);

console.log('Заканчиваем');

Что выведется в консоль? Сначала «Начинаем», потом «Заканчиваем», и только через 2 секунды — «Прошло 2 секунды». Это и есть асинхронность: код не ждёт, а идёт дальше.

Колбэки — это просто, но у них есть страшный недостаток — «ад колбэков» (callback hell). Когда одна асинхронная операция зависит от другой, вы вкладываете колбэки друг в друга, и код превращается в матрёшку, которую невозможно читать.

getUser(1, (user) => {
  getPosts(user.id, (posts) => {
    getComments(posts[0].id, (comments) => {
      console.log(comments);
    });
  });
});

Согласитесь, выглядит жутковато. Читать такой код — то ещё удовольствие, а если что-то сломается, вы будете долго искать, где именно.

Промисы: спасатели с чётким планом

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

  • pending — ожидание, операция ещё не завершена;
  • fulfilled — операция завершилась успешно;
  • rejected — операция завершилась с ошибкой.

Промисы позволяют писать асинхронный код более линейно, с помощью цепочек .then() и .catch(). Например:

getUser(1)
  .then(user => getPosts(user.id))
  .then(posts => getComments(posts[0].id))
  .then(comments => console.log(comments))
  .catch(error => console.error(error));

Уже лучше, правда? Вместо вложенных колбэков — плоская цепочка. Но есть нюанс: если операций много, цепочка становится длинной, и всё равно приходится следить за тем, что передаётся из одного .then() в другой.

Правило: всегда добавляйте .catch() в конец цепочки промисов. Иначе ошибки будут проглатываться молча, и вы не поймёте, почему код не работает.

Async/await: сахар, который делает жизнь слаще

В 2017 году появился синтаксис async/await. Это просто синтаксический сахар над промисами, но он делает код ещё более читаемым. Вы пишете асинхронный код так, будто он синхронный, но при этом не блокируете поток.

async function showComments() {
  try {
    const user = await getUser(1);
    const posts = await getPosts(user.id);
    const comments = await getComments(posts[0].id);
    console.log(comments);
  } catch (error) {
    console.error(error);
  }
}

Красота! Читается как обычный синхронный код, но работает асинхронно. Обратите внимание на try/catch — это аналог .catch() для промисов.

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

Практические советы и лайфхаки

Теперь, когда мы разобрались с основами, давайте поговорим о том, как не наступить на грабли.

1. Не блокируйте поток

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

const [user, posts] = await Promise.all([
  getUser(1),
  getPosts(1)
]);

Это сократит время ожидания, потому что обе операции выполнятся одновременно.

2. Обрабатывайте ошибки сразу

Ошибки в асинхронном коде — это боль. Если вы используете async/await, обязательно оборачивайте код в try/catch. Если используете промисы — не забывайте про .catch(). И никогда не оставляйте промис без обработчика ошибок.

3. Используйте finally для очистки

Если вам нужно выполнить код после завершения асинхронной операции независимо от того, успешно она завершилась или с ошибкой (например, закрыть загрузчик), используйте finally.

try {
  await saveData();
} catch (error) {
  showError(error);
} finally {
  hideLoader();
}

4. Не забывайте про Promise.race()

Если вам нужно выполнить операцию с таймаутом, Promise.race() — ваш друг. Он принимает массив промисов и возвращает результат самого быстрого.

const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000));
const data = await Promise.race([fetchData(), timeout]);

Сравнение способов работы с асинхронностью

Способ Плюсы Минусы
Колбэки Простота, работает везде Ад колбэков, сложно читать
Промисы Читаемость, обработка ошибок Длинные цепочки, неудобно для сложных сценариев
Async/await Максимальная читаемость, линейный код Требует поддержки, можно забыть try/catch

Что в итоге?

Асинхронность — это не страшно, если знать инструменты. Начните с промисов, если вы новичок, и постепенно переходите на async/await — это стандарт современной разработки. Главное — не забывайте обрабатывать ошибки и не блокируйте поток.

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

Если хотите, чтобы ваш проект разрабатывали профессионалы, которые знают все тонкости асинхронности — обращайтесь в Digital-студию WNDER.

© Студия WNDER