Как вернуть ответ из асинхронного вызова: полный гайд для JS

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

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

Начать курс

«Я запускаю функцию, а она возвращает undefined!» — знакомая боль?

Представьте: вы пишете код на JavaScript, делаете запрос к API, ждёте данные… а в ответ получаете undefined. Или хуже — Promise {<pending>}. Руки опускаются, хочется закрыть браузер и уйти в монахи.

Не спешите. Эта проблема мучает даже опытных разработчиков. Не зря вопрос «How do I return the response from an asynchronous call?» на Stack Overflow собрал тысячи просмотров и сотни ответов. Сегодня мы разберём его раз и навсегда.

В этой статье вы узнаете:

  • Почему return не работает так, как вы привыкли
  • Как приручить асинхронность с помощью колбэков, промисов и async/await
  • Какой подход выбрать для вашего проекта

Поехали!

Почему return не возвращает данные?

Давайте сразу к делу. Вот классический пример:

function getUserData(userId) {
  fetch(`https://api.example.com/users/${userId}`)
    .then(response => response.json())
    .then(data => {
      return data; // Кажется, что всё ок?
    });
}

const user = getUserData(42);
console.log(user); // undefined

Знакомо? Вы написали return data внутри .then(), но снаружи получили undefined. Почему?

JavaScript — однопоточный и неблокирующий. Когда вы вызываете fetch, он не ждёт ответа от сервера. Вместо этого он «откладывает» выполнение колбэка в сторонку и сразу переходит к следующей строке. А следующая строка — это console.log(user), где user ещё не определён.

Ваша функция getUserData завершается раньше, чем приходит ответ от сервера. Она ничего не возвращает (возвращает undefined), а данные «висят» в воздухе.

Как это исправить? Есть три основных способа.

Способ 1: Колбэки (Callbacks) — дедушка асинхронности

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

function getUserData(userId, callback) {
  fetch(`https://api.example.com/users/${userId}`)
    .then(response => response.json())
    .then(data => {
      callback(data); // Вызываем колбэк с данными
    })
    .catch(error => {
      callback(null, error); // Или передаём ошибку
    });
}

// Использование:
getUserData(42, (data, error) => {
  if (error) {
    console.error('Ошибка:', error);
    return;
  }
  console.log('Данные пользователя:', data);
});

Плюсы: простота, работает везде.

Минусы: если вложить несколько колбэков друг в друга, получится «ад колбэков» (callback hell). Код становится нечитаемым.

getUserData(42, (data1) => {
  getPosts(data1.id, (data2) => {
    getComments(data2[0].id, (data3) => {
      // ... и так далее
    });
  });
});

Современный JavaScript от этого ушёл. Но понимать колбэки нужно — они лежат в основе всех остальных подходов.

Способ 2: Промисы (Promises) — спасители читаемости

Промис — это объект, который представляет собой «обещание» вернуть значение в будущем. У него есть три состояния: pending (ожидание), fulfilled (выполнено) и rejected (отклонено).

Вернёмся к нашему примеру. Вместо колбэка мы возвращаем сам промис:

function getUserData(userId) {
  return fetch(`https://api.example.com/users/${userId}`)
    .then(response => {
      if (!response.ok) {
        throw new Error('Сеть не в порядке');
      }
      return response.json();
    });
}

// Использование:
const userPromise = getUserData(42);

userPromise
  .then(data => {
    console.log('Данные пользователя:', data);
  })
  .catch(error => {
    console.error('Ошибка:', error);
  });

Теперь getUserData возвращает промис. Мы не можем сразу получить данные, но можем «подписаться» на них через .then().

Важный момент: вы не можете вернуть данные из промиса синхронно. Это физически невозможно. Но вы можете вернуть сам промис и работать с ним дальше.

Цепочки промисов

Промисы решают проблему «ада колбэков» — их можно выстраивать в цепочки:

function getUserData(userId) {
  return fetch(`https://api.example.com/users/${userId}`)
    .then(response => response.json());
}

function getPosts(userId) {
  return fetch(`https://api.example.com/users/${userId}/posts`)
    .then(response => response.json());
}

getUserData(42)
  .then(user => getPosts(user.id))
  .then(posts => {
    console.log('Посты пользователя:', posts);
  })
  .catch(error => {
    console.error('Где-то ошибка:', error);
  });

Код стал плоским и читаемым. Одна ошибка в любом месте цепочки — и мы ловим её в одном .catch().

Способ 3: Async/Await — сахар, который вы полюбите

Начиная с ES2017, в JavaScript появился синтаксический сахар над промисами — async/await. Он позволяет писать асинхронный код так, будто он синхронный.

async function getUserData(userId) {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  if (!response.ok) {
    throw new Error('Сеть не в порядке');
  }
  const data = await response.json();
  return data; // Теперь это работает!
}

// Использование:
async function main() {
  try {
    const user = await getUserData(42);
    console.log('Данные пользователя:', user);
  } catch (error) {
    console.error('Ошибка:', error);
  }
}

main();

Обратите внимание: async перед функцией делает её асинхронной. Внутри неё можно использовать await, который «ждёт» выполнения промиса. Сама функция при этом возвращает промис.

Ключевой момент: return data внутри async функции работает именно так, как вы ожидали! Потому что await приостанавливает выполнение функции до получения результата.

Ловушка: забыли async

Самая частая ошибка новичков — использовать await без async:

function getUserData(userId) {
  const response = await fetch(`...`); // SyntaxError
  return response.json();
}

Или пытаться использовать

Блоги

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