← Курс/Промисы, async/await, колбэки — объясни разницу#130 из 257+40 XP

Промисы, async/await, колбэки — объясни разницу

Краткий ответ

Все три — способы работы с асинхронным кодом. Колбэки — исторически первый подход, но ведут к "callback hell" при вложенности. Промисы решают эту проблему через цепочки .then() и стандартизированную обработку ошибок. async/await — синтаксический сахар над промисами, делающий асинхронный код похожим на синхронный. Важно: await всегда ждёт промис последовательно — для параллельности нужен Promise.all.

Полный разбор

Колбэки (Callbacks)

Первый способ асинхронности — передать функцию, которую надо вызвать "когда будет готово":

function fetchUser(id, callback) {
  setTimeout(() => {
    if (id <= 0) {
      callback(new Error('Неверный ID'))  // ошибка — первым аргументом
    } else {
      callback(null, { id, name: 'Alice' })  // успех — вторым
    }
  }, 100)
}

// Node.js-стиль: первый аргумент — ошибка (error-first callback)
fetchUser(1, (err, user) => {
  if (err) { console.error(err); return }
  console.log(user)
})

**Callback Hell** — проблема глубокой вложенности:

fetchUser(1, (err, user) => {
  if (err) return handleError(err)
  fetchPosts(user.id, (err, posts) => {
    if (err) return handleError(err)
    fetchComments(posts[0].id, (err, comments) => {
      if (err) return handleError(err)
      // Ещё глубже...
      // "Pyramid of Doom"
    })
  })
})

Проблемы колбэков: вложенность, обработка ошибок в каждом колбэке вручную, невозможность использовать try/catch, трудно читать и отлаживать.

Промисы (Promises)

Промис — объект, представляющий результат асинхронной операции. Три состояния:

pending (ожидание) → fulfilled (выполнен) → resolved value
                   → rejected (отклонён)  → error reason
function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id <= 0) reject(new Error('Неверный ID'))
      else resolve({ id, name: 'Alice' })
    }, 100)
  })
}

// Цепочка — вместо вложенности:
fetchUser(1)
  .then(user => fetchPosts(user.id))      // возвращает промис
  .then(posts => fetchComments(posts[0].id))
  .then(comments => console.log(comments))
  .catch(err => console.error(err))       // одна точка обработки ошибок
  .finally(() => console.log('Завершено'))

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

// Параллельное выполнение — ждём ВСЕ
Promise.all([fetchA(), fetchB(), fetchC()])
  .then(([a, b, c]) => console.log(a, b, c))
  // Если хоть один rejected — весь Promise.all rejected

// Ждём первый успешный
Promise.any([fetchA(), fetchB()])
  .then(result => console.log(result))

// Ждём первый завершённый (fulfilled или rejected)
Promise.race([fetchA(), timeout(5000)])
  .then(result => console.log(result))

// Как Promise.all, но не падает при ошибке
Promise.allSettled([fetchA(), fetchB()])
  .then(results => results.forEach(r => console.log(r.status)))

async/await

Синтаксический сахар над промисами. async функция всегда возвращает промис. await приостанавливает выполнение до resolve/reject.

async function loadData() {
  try {
    const user = await fetchUser(1)         // ждём промис
    const posts = await fetchPosts(user.id) // ждём следующий
    const comments = await fetchComments(posts[0].id)
    return comments
  } catch (err) {
    console.error('Ошибка:', err)
    throw err  // пробрасываем если нужно
  }
}

// async функция возвращает промис
loadData().then(data => console.log(data))

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

// МЕДЛЕННО: последовательно (3 секунды если каждый по 1 сек)
async function sequential() {
  const a = await fetchA()  // 1 секунда
  const b = await fetchB()  // + 1 секунда
  const c = await fetchC()  // + 1 секунда
  return [a, b, c]          // = 3 секунды
}

// БЫСТРО: параллельно (1 секунда)
async function parallel() {
  const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()])
  return [a, b, c]  // = 1 секунда (все запросы одновременно)
}

**Правило**: если операции не зависят друг от друга — всегда используй Promise.all.

Частые ошибки

// ОШИБКА 1: забытый await
async function bad() {
  const user = fetchUser(1)  // без await — получим промис, не данные!
  console.log(user.name)     // undefined
}

// ОШИБКА 2: не обработан rejected promise
fetchUser(-1).then(user => console.log(user))
// UnhandledPromiseRejection — всегда добавляй .catch()!

// ОШИБКА 3: await в forEach (не работает)
async function badLoop(ids) {
  ids.forEach(async (id) => {
    const user = await fetchUser(id)  // forEach не ждёт!
    console.log(user)
  })
}

// ПРАВИЛЬНО: for...of с await
async function goodLoop(ids) {
  for (const id of ids) {
    const user = await fetchUser(id)
    console.log(user)
  }
}

Когда что использовать

Колбэки  → только в API которые их требуют (addEventListener, fs в Node.js)
Промисы  → когда нужны Promise.all/race/any, явные цепочки
async/await → для большинства асинхронного кода — читаемо и понятно

Связанные уроки курса

  • Колбэки
  • Промисы
  • async/await
  • Promise chain
  • Ошибки промисов
  • Как отвечать на собеседовании

    **Расскажи эволюцию**: "Сначала были колбэки → callback hell → промисы решили вложенность → async/await сделали код читаемым".

    **Покажи одну задачу тремя способами** — это отличный способ продемонстрировать понимание. Загрузка пользователя через колбэк, потом промис, потом async/await.

    **Обязательно упомяни**: разницу между последовательным await и Promise.all — это часто спрашивают как follow-up вопрос.

    **Время ответа**: 4-5 минут.

    Красные флаги ответа

    1. **"async/await заменяет промисы"** — нет, async/await — это синтаксический сахар. Внутри всё равно промисы. await fetch(url) ждёт промис.

    2. **Не знать про await в forEach** — это классический баг в реальном коде. Если не знаешь почему forEach не работает с async — это практический пробел.

    3. **Не знать Promise.all** — если для параллельных запросов пишешь последовательные await — ты тормозишь приложение. Любой ревьюер это заметит.

    Примеры

    Одна и та же задача: получить пользователя и его посты — через колбэки, затем промисы, затем async/await

    // Имитация API с задержкой (без fetch — используем Promise)
    function delay(ms) {
      return new Promise(resolve => setTimeout(resolve, ms))
    }
    
    const fakeDB = {
      users: { 1: { id: 1, name: 'Alice' }, 2: { id: 2, name: 'Bob' } },
      posts: {
        1: [{ id: 10, title: 'Первый пост' }, { id: 11, title: 'Второй пост' }],
        2: [{ id: 20, title: 'Пост Bob' }]
      }
    }
    
    // ===== СПОСОБ 1: КОЛБЭКИ =====
    function getUserCb(id, callback) {
      delay(50).then(() => {
        const user = fakeDB.users[id]
        if (!user) callback(new Error(`Пользователь ${id} не найден`))
        else callback(null, user)
      })
    }
    
    function getPostsCb(userId, callback) {
      delay(50).then(() => {
        const posts = fakeDB.posts[userId] || []
        callback(null, posts)
      })
    }
    
    console.log('=== Колбэки ===')
    getUserCb(1, (err, user) => {
      if (err) { console.error('Ошибка:', err.message); return }
      console.log('Пользователь:', user.name)
    
      getPostsCb(user.id, (err, posts) => {
        if (err) { console.error('Ошибка:', err.message); return }
        console.log('Постов:', posts.length)
        // Если нужны комментарии — ещё один уровень...
        // Это и есть callback hell
      })
    })
    
    // ===== СПОСОБ 2: ПРОМИСЫ =====
    function getUser(id) {
      return delay(50).then(() => {
        const user = fakeDB.users[id]
        if (!user) throw new Error(`Пользователь ${id} не найден`)
        return user
      })
    }
    
    function getPosts(userId) {
      return delay(50).then(() => fakeDB.posts[userId] || [])
    }
    
    console.log('\n=== Промисы ===')
    getUser(1)
      .then(user => {
        console.log('Пользователь:', user.name)
        return getPosts(user.id)
      })
      .then(posts => {
        console.log('Постов:', posts.length)
        return posts
      })
      .catch(err => console.error('Ошибка:', err.message))
      .finally(() => console.log('Промис-цепочка завершена'))
    
    // ===== СПОСОБ 3: ASYNC/AWAIT =====
    async function loadUserWithPosts(userId) {
      try {
        const user = await getUser(userId)
        console.log('\n=== async/await ===')
        console.log('Пользователь:', user.name)
    
        const posts = await getPosts(user.id)
        console.log('Постов:', posts.length)
    
        return { user, posts }
      } catch (err) {
        console.error('Ошибка:', err.message)
        throw err
      }
    }
    
    loadUserWithPosts(1).then(data => {
      console.log('Готово:', data.user.name, '—', data.posts.length, 'постов')
    })
    
    // ===== ПАРАЛЛЕЛЬНО vs ПОСЛЕДОВАТЕЛЬНО =====
    async function comparePerformance() {
      await delay(200) // ждём предыдущие примеры
    
      console.log('\n=== Параллельно vs Последовательно ===')
    
      // Последовательно: 50ms + 50ms = ~100ms
      const seqStart = Date.now()
      const user1 = await getUser(1)
      const user2 = await getUser(2)
      console.log(`Последовательно: ${Date.now() - seqStart}ms`)
    
      // Параллельно: max(50ms, 50ms) = ~50ms
      const parStart = Date.now()
      const [u1, u2] = await Promise.all([getUser(1), getUser(2)])
      console.log(`Параллельно:    ${Date.now() - parStart}ms`)
    
      console.log('Пользователи:', u1.name, u2.name)
    }
    
    comparePerformance()