Gernar
Frontend DeveloperJavaScript, асинхронность

Интервью-вопрос

Что использовали раньше до появления Promise в JavaScript

До Promise асинхронный код чаще строили на callback-функциях, событиях, Deferred-объектах и библиотеках для контроля потока. На интервью важно не просто перечислить их, а объяснить, какие проблемы Promise помог решить.

Добавлен
Редакция

Подготовьте короткий ответ и пару деталей на случай уточняющих вопросов.

🐰0
🥚0

Мини-квиз

Проверка перед разбором

Несколько быстрых вопросов перед разбором. Так проще поймать места, которые только кажутся понятными.

Вопрос 1 из 50 правильно

Что лучше назвать первым в ответе?

Вы отвечаете на вопрос, что использовали до появления Promise.

Варианты ответа

Разбор

Разобраться, а не зазубрить

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

Базовая идея

Короткий ответ: до Promise основой были callback-функции. Функция запускала асинхронную работу, а результат отдавала не через return, а через вызов переданной функции.

loadUser(userId, function (err, user) {
  if (err) {
    showError(err);
    return;
  }

  renderUser(user);
});

Такой код сам по себе нормальный. Проблема появляется, когда следующий шаг зависит от предыдущего, ошибок много, а UI должен оставаться в согласованном состоянии. Тогда код быстро разрастается и становится хрупким. Легко забыть показать ошибку, убрать индикатор загрузки или остановить обновление состояния после ухода со страницы.

Что важно сказать на интервью

Хороший ответ не должен звучать как простое перечисление: раньше были колбэки, потом стало лучше. Лучше покажите эволюцию:

  • callback-функции были базовым способом получить результат позже;
  • для ошибок часто использовали соглашение err первым аргументом;
  • для событийных потоков применяли EventEmitter и похожие API;
  • для управления несколькими callback-задачами брали библиотеки вроде Async.js;
  • в браузерном legacy-коде встречались jQuery Deferred и похожие модели.

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

Какие старые подходы упомянуть

Один результатcallback

Подходит для простого старого API. В UI может быть опасен без защиты от двойного вызова, пропущенной ошибки и обновления состояния после размонтирования.

Цепочка шаговAsync.js

Помогает задать порядок сетевых шагов и не показывать экран раньше данных. Минусы: лишняя зависимость и не всегда привычный Promise-контракт.

Поток событийEventEmitter

Хорош для data, close, error и похожих событий. Во frontend важно снять подписку в cleanup и не обновлять UI после закрытого потока.

Старый браузерный кодjQuery Deferred

Встречается в legacy-проектах. При смешивании с Promise проверяйте порядок обработчиков, ошибки и состояние loading/error.

Главная боль callback-подхода

Самая известная проблема называлась callback hell или pyramid of doom. Это ситуация, когда шаги вложены друг в друга. Каждый новый шаг увеличивает отступ, а вместе с ним сложность чтения.

Плохой пример. Так лучше не писать в новом коде:

loadUser(userId, function (err, user) {
  if (err) return showError(err);

  loadOrders(user.id, function (err, orders) {
    if (err) return showError(err);

    loadRecommendations(orders, function (err, recommendations) {
      if (err) return showError(err);

      renderPage(user, orders, recommendations);
    });
  });
});

Что здесь ломается на практике: обработка ошибок повторяется, код сложно тестировать, трудно добавить отмену, таймаут или промежуточное состояние загрузки. В UI это часто приводит к неочевидным багам. Старый запрос может перерисовать экран после нового, кнопка может навсегда остаться в состоянии loading, а ошибка может потеряться между уровнями вложенности.

Безопаснее разнести шаги на небольшие функции, вернуть из них Promise и в месте вызова держать один путь для loading, error и успешного состояния. В React дополнительно проверяйте отмену запроса или актуальность ответа в useEffect. Так вы не обновите состояние после размонтирования или после более нового запроса.

Как помогали библиотеки и события

До широкого использования Promise разработчики не всегда писали всю вложенность вручную. Библиотеки вроде Async.js давали готовые функции для последовательного и параллельного запуска задач. Это помогало не показать UI раньше нужных данных и не запускать зависимый запрос слишком рано. Но базовый факт не менялся: под капотом оставался callback-контракт.

EventEmitter решал другую задачу. Он хорошо подходил не для одного результата, а для серии событий: данные пришли, поток закрылся, произошла ошибка. На интервью стоит сказать, что event-based API и callback API похожи тем, что оба вызывают функции позже. Но модель у них разная.

Важно не говорить, что события были старой версией Promise. Promise обычно представляет один будущий успех или одну ошибку. EventEmitter может отправлять много событий. Если не снять подписку или не обработать error, можно получить утечку памяти, повторные обновления интерфейса или падение процесса в Node.js.

Где здесь jQuery Deferred

В старом frontend-коде часто встречался jQuery Deferred. Он позволял подписываться на завершение асинхронной операции и строить цепочки обработчиков. По смыслу он близок к Promise, но исторически появился до полной стандартизации. В старых версиях он не всегда вел себя как нативный Promise.

Безопасная формулировка для интервью: были Deferred-объекты, например в jQuery. Они решали похожую задачу, но не были тем же самым стандартным Promise-контрактом. Поэтому при миграции legacy-кода их поведение нужно проверять.

Как связать это с современным кодом

Если вас спрашивают про прошлые подходы, обычно ждут не учебник истории, а понимание, зачем Promise стали полезны. Promise дал объект, который можно вернуть из функции, передать дальше и связать с другими операциями через then, catch и позже через async/await.

Пример безопасной миграции callback API:

function loadUserPromise(userId) {
  return new Promise((resolve, reject) => {
    loadUser(userId, function (err, user) {
      if (err) {
        reject(err);
        return;
      }

      resolve(user);
    });
  });
}

Так старый API получает более удобный внешний контракт. Но важно вызвать либо resolve, либо reject, не дергать их повторно из разных веток и не проглотить ошибку. Если этот код используется для запроса с экрана, рядом все равно нужна защита от устаревшего ответа: например AbortController для fetch или флаг актуальности в cleanup эффекта.

Безопаснее объяснить
  1. 1Начать с callback как базового механизма
  2. 2Объяснить проблему вложенности и ошибок
  3. 3Упомянуть EventEmitter, Async.js и Deferred по назначению
  4. 4Сказать, что Promise дал стандартный способ связывать операции
Опасно на интервью
  1. 1Сказать, что раньше ничего не было
  2. 2Смешать callback, event и Promise в одно понятие
  3. 3Не объяснить, что ломалось при поддержке кода
  4. 4Назвать async/await полной заменой всех старых API

Практический вывод

На интервью можно ответить так:

До Promise чаще использовали callback-функции. Асинхронная операция принимала функцию-обработчик и вызывала ее с результатом или ошибкой. В Node.js был распространен error-first callback. Для событийных сценариев использовали EventEmitter, а для управления несколькими задачами брали библиотеки вроде Async.js. В браузерном legacy-коде встречался jQuery Deferred. Главная проблема старого подхода была не в callback как таковом, а в сложной композиции: вложенность, повторяющаяся обработка ошибок и риск потерять состояние. Promise стандартизировал будущий результат и сделал цепочки и ошибки понятнее.

Эту формулировку можно сократить до 3-4 предложений, если интервью идет быстро. Главное не забыть практический смысл: Promise появился как ответ на реальные проблемы поддержки асинхронного кода. Для frontend это значит меньше риска потерять ошибку, оставить вечный loading, выполнить лишний запрос или обновить экран устаревшими данными.

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

Где обычно ошибаются

Проверьте формулировки, которые звучат уверенно, но на интервью быстро выдают пробелы.

  1. 1

    Говорить только про callback hell

    Callback hell важен, но одного этого термина мало. Добавьте обработку ошибок, композицию операций и старые инструменты вроде EventEmitter, Async.js и Deferred. Так ваш ответ будет звучать практично, а не как заученная фраза.
  2. 2

    Не упоминать ошибки в callback API

    В старом Node.js стиле часто использовали error-first callback: первым аргументом шел err. Если забыть проверку ошибки, UI может показать успешное состояние после проваленного запроса или потерять причину сбоя.
  3. 3

    Приравнивать Deferred к Promise

    Старые jQuery Deferred были похожи по идее, но не всегда совпадали со стандартным поведением Promise. При миграции это может сломать цепочку обработчиков или обработку исключений. Поэтому лучше назвать их предшественником, а не полным аналогом.
  4. 4

    Называть EventEmitter заменой Promise

    EventEmitter решает другую задачу: несколько событий во времени, а не один будущий результат. Если использовать его как замену Promise для одного запроса, легко получить лишние подписки, повторные обновления UI и утечки памяти.
  5. 5

    Забывать про современный вывод

    На интервью важно не только перечислить историю, но и объяснить, зачем появились Promise. Сильный ответ связывает старые проблемы с современным кодом: меньше вложенности, понятнее цепочки и проще собрать ошибки в одном месте через catch или try/catch с async/await.

Follow-up

Что могут спросить дальше

Короткие ответы на вопросы, которыми проверяют понимание старых подходов к асинхронности и их ограничений.

Живые ответы

Видео с похожим вопросом

Если найдем публичные интервью с таким вопросом, добавим их сюда. Их удобно смотреть после теории, чтобы свериться с живыми ответами.

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

Содержание