Разрешите представить вам перевод статьи Нолана Лоусона «У нас проблемы с промисами», одной из лучших по теме из тех, что мне доводилось читать.

У нас проблемы с промисами


Дорогие JavaScript разработчики, настал момент признать это — у нас проблемы с промисами.

Нет, не с самими промисами. Их реализация по спецификации A+ превосходна. Основная проблема, которая сама предстала передо мной за годы наблюдений за тем, как многие программисты борются с богатыми на промисы API, заключается в следующем:

— Многие из нас используют промисы без действительного их понимания.

Если вы мне не верите, решите такую задачку:

Вопрос: В чем разница между этими четырьмя вариантами использования промисов?

doSomething().then(function () {
  return doSomethingElse();
});

doSomething().then(function () {
  doSomethingElse();
});

doSomething().then(doSomethingElse());

doSomething().then(doSomethingElse);


Если вы знаете ответ, то разрешите вас поздравить — по части промисов вы ниндзя. Пожалуй, дальше этот пост вам можно не читать.

Остальным 99,99% из вас я спешу сказать, чтобы вы не расстраивались, вы в хорошей компании. Никто из тех, кто ответил на мой твит, не смог решить задачу. Даже я был очень удивлен ответом на 3-й вопрос. Да-да, несмотря на то, что это я его задал!

Ответ на задачу приведен в самом конце статьи, но сначала я хотел бы объяснить, почему промисы в первом приближении такие коварные, и почему многие из нас, новички и эксперты, попадаются в их ловушки. Также я хочу предложить вам свое решение, один необычный трюк, который поможет лучше понять промисы. И, разумеется, я верю, что после моего объяснения они уже не покажутся вам такими сложными.

Прежде, чем начать, давайте обозначим некоторые моменты.

Почему промисы?


Если вы читаете статьи о промисах, то часто будете встречать отсылки на «пирамиду зла» («pyramide of doom» в ориг.), образованную ужасным кодом на колбэках, который растягивает страницу направо за экран.

Промисы действительно решают эту проблему, но это нечто большее, чем просто уменьшение отступов. Как объясняется в замечательной беседе «Спасение из ада колбэков», настоящая их проблема в том, что они лишают нас возможности использовать инструкции return и throw. Вместо этого логика наших программ основана на использовании побочных эффектов, когда одна функция вызывает другую.

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

Весь смысл промисов в том, чтобы вернуть нам основы языка, потерянные в момент нашего перехода на асинхронность: return, throw и стек. Но вы должны знать, как правильно использовать промисы, чтобы подняться в этом на более высокий уровень.

Ошибки новичков


Кто-то пытается объяснить промисы в виде мультика или, говоря словами: «О! Это штука, которую вы можете создать и передавать всюду, а она собой символизирует какое-то значение, получаемое и возвращаемое асинхронно».

Я не нахожу такое объяснение достаточно полезным. Для меня промисы — это всегда часть структуры кода, его потока выполнения.

Небольшое отступление: термин «промисы» для разных людей несет в себе разный смысл. В этой статье я буду рассказывать об официальной спецификации, доступной в современных браузерах как window.Promise. Для тех браузеров, которые не имеют window.Promise, есть хороший полифил с нахальным названием Lie (ложь), содержащий минимальную реализацию спецификации.

Ошибка новичка №1 — «пирамида зла» из промисов


Глядя, как люди используют PouchDB, API которого сильно завязано на промисах, я вижу немало плохих паттернов их использования. Вот наиболее распространенный пример:

remotedb.allDocs({
  include_docs: true,
  attachments: true
}).then(function (result) {
  var docs = result.rows;
  docs.forEach(function(element) {
    localdb.put(element.doc).then(function(response) {
      alert("Pulled doc with id " + element.doc._id + " and added to local db.");
    }).catch(function (err) {
      if (err.status == 409) {
        localdb.get(element.doc._id).then(function (resp) {
          localdb.remove(resp._id, resp._rev).then(function (resp) {
// и так далее…

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

Если вы думаете, что подобные ошибки совершают только абсолютные новички, я вас удивлю — код примера выше взят из официального блога разработчиков BlackBerry! От старой привычки использовать колбэки избавиться трудно.

Вот вариант получше:

remotedb.allDocs(...)
  .then(function (resultOfAllDocs) {
    return localdb.put(...);
  })
  .then(function (resultOfPut) {
    return localdb.get(...);
  })
  .then(function (resultOfGet) {
    return localdb.put(...);
  })
  .catch(function (err) {
    console.log(err);
  });

В примере выше использовались составные промисы («composing promises» в ориг.) — одна из сильнейших сторон промисов. Каждая последующая функция будет вызвана, когда предыдущий промис «зарезолвится», и вызвана она будет с результатом работы предыдущего промиса. Подробности будут ниже.

Ошибка новичка №2 — как мне использовать forEach() с промисами?


Это тот момент, когда понимание промисов большинством людей начинает сдавать. Они хорошо знакомы с итераторами forEach, for или while, но не имеют ни малейшего представления, как сочетать их с промисами. Тогда рождается что-то подобное:

// Я хочу применить remove() ко всем документам
db.allDocs({include_docs: true})
  .then(function (result) {
    result.rows.forEach(function (row) {
      // Метод remove возвращает promise
      db.remove(row.doc);
    });
  })
  .then(function () {
    // А здесь я наивно уверен, что все документы уже удалены!
  });

Что не так с этим кодом? Проблема в том, что первая функция возвращает undefined, а значит вторая не ждет окончания выполнения db.remove() для всех документов. На самом деле она вообще ничего не ждет и выполнится, когда будет удалено любое число документов, а может и ни одного.

Это очень коварная ошибка, потому что поначалу вы можете ее даже не заметить, особенно, если документы будут удаляться достаточно быстро для обновления интерфейса. Бага может всплывать только в редких случаях, не во всех браузерах, а значит, выявить и устранить ее будет практически невозможно.

Подводя итог, скажу, что конструкции типа forEach, for и while «не те дроны, что вы ищете». Вам нужен Promise.all():

db.allDocs({include_docs: true})
  .then(function (result) {
    var arrayOfPromises = result.rows.map(function (row) {
      return db.remove(row.doc);
    });
    return Promise.all(arrayOfPromises);
  })
  .then(function (arrayOfResults) {
    // Вот теперь все документы точно удалены!
  });

Что здесь происходит? Promise.all() принимает в качестве аргумента массив промисов и возвращает новый промис, который «зарезолвится», только когда все документы будут удалены. Это асинхронный эквивалент цикла for.

Также промис из Promise.all() передаст в следующую функцию массив результатов, что может быть очень удобно, если вы, например, не удаляете документы, а получаете данные сразу из нескольких источников. Если хотя бы один промис из массива, переданного в Promise.all() «зареджектится», то и результирующий промис перейдет в состояние rejected.

Ошибка новичка №3 — забываем добавлять .catch()


Это еще одна распространенная ошибка — блаженно верить, что ваши примисы никогда не вернут ошибку. Многие разработчики просто забывают добавлять catch() куда-либо в своем коде.

К сожалению, часто это означает, что ошибки будут «проглочены». Вы даже никогда не узнаете, что они были — особая боль при отладке приложения.

Чтобы избежать этого неприятного сценария, я взял за правило, которое затем переросло в привычку, всегда добавлять в конец моей цепочки промисов метод catch():

somePromise().then(function () {
    return anotherPromise();
  })
  .then(function () {
    return yetAnotherPromise();
  })
  // простое и полезное окончание цепочки промисов:
  .catch(console.log.bind(console));

Даже если вы гарантированно, стопроцентно не ожидаете каких-либо ошибок, добавление catch() будет разумным решением. Потом, если вдруг ваше предположение насчет ошибок не оправдается, вы скажете себе спасибо.

Ошибка новичка №4 — использование «deferred»


Такую ошибку я вижу постоянно и не хочу даже повторять название этого объекта, опасаясь, что он, подобно Битлджусу из одноименного фильма, только того и ждет, чтобы увеличить число случаев своего использования.

Вкратце, в своем развитии промисы прошли долгую историю. У JavaScript сообщества ушло много времени на то, чтобы реализовать их правильно. Поначалу jQuery и Angular повсеместно использовали паттерн deferred-объектов, который впоследствии был заменен на спецификацию промисов ES6, в основе которой лежали «хорошие» библиотеки Q, When, RSVP, Bluebird, Lie и другие.

В общем, если вы вдруг написали это слово в своем коде (я не повторю его в третий раз!), знайте — вы что-то делаете не так. Ниже рецепт, как этого избежать.

Большинство «промисных» библиотек дают вам возможность импортировать промисы из других библиотек. Например, модуль $q из Angular позволяет вам обернуть не-$q промисы при помощи $q.when(). То есть пользователи Angular могут оборачивать промисы из PouchDB так:

// это все, что нужно:
$q.when(db.put(doc)).then(/* ... */);

Другой путь — использование паттерна раскрытия конструктора («revealing constructor pattern» в ориг.). Он удобен для оборачивания API, не использующего промисы. Например, чтобы обернуть основанное на колбэках API Node.js, вы можете сделать следующее:

new Promise(function (resolve, reject) {
  fs.readFile('myfile.txt', function (err, file) {
    if (err) {
      return reject(err);
    }
    resolve(file);
  });
}).then(/* ... */)

Готово! Мы расправились с ужасным defer… Ах, чуть не произнес в третий раз! :)

Ошибка новичка №5 — использование внешних функций вместо возвращения результата


Что не так с этим кодом?

somePromise().then(function () {
    someOtherPromise();
  })
  .then(function () {
    // Ох, я надеюсь someOtherPromise «зарезолвился»…
    // Осторожно, спойлер: нет, не «зарезолвился».
  });

Хорошо, сейчас идеальный момент для того, чтобы поговорить обо всем том, что вы вообще должны знать о промисах.

Серьезно, это тот самый трюк, поняв который, вы сами сможете избежать всех тех ошибок, о которых мы говорили. Вы готовы?

Как я уже упоминал, магия промисов в том, что они возвращают нам драгоценные return и throw. Но что это означает на практике?

Каждый промис предоставляет вам метод then() (а еще catch(), который на практике просто «сахар» для then(null, …)). И вот мы внутри функции then():

somePromise().then(function () {
  // Вау, мы внутри функции then()!
});

Что мы можем тут сделать? Три вещи:

  1. Вернуть (return) другой промис
  2. Вернуть (return) синхронное значение (или undefined)
  3. Выдать (throw) синхронную ошибку

Вот и все, весь трюк. Поймете его — поймете промисы. Давайте теперь разберем подробно каждый из пунктов.

1. Вернуть другой промис


Это частый паттерн, который вы могли видеть во всевозможной литературе о промисах, а также в примере с составными промисами выше:

getUserByName('nolan').then(function (user) {
    // Функция getUserAccountById возвращает promise,
    // результат которого попадет в следующий then
    return getUserAccountById(user.id);
  })
  .then(function (userAccount) {
    // Я знаю все о пользователе!
  });

Обратите внимание, что я именно возвращаю второй промис, использую return. Использование здесь return — это ключевой момент. Если бы я просто вызвал getUserAccountById, то да, был бы запрос за данными пользователя, был бы получен результат, который нигде бы не пригодился — следующий then получил бы undefined вместо желанного userAccount.

2. Вернуть синхронное значение (или undefined)


Возвращение undefined в качестве результата — частая ошибка. А вот возвращение какого-либо синхронного значения — отличный способ преобразовать синхронный код в цепочку промисов. Допустим, у нас в памяти есть кэш данных о пользователях. Мы можем:

getUserByName('nolan').then(function (user) {
    if (inMemoryCache[user.id]) {
      // Данные этого пользователя уже есть,
      // возвращаем сразу
      return inMemoryCache[user.id];
    }
    // А вот про этого пока не знаем,
    // вернем промис запроса
    return getUserAccountById(user.id);
  })
  .then(function (userAccount) {
    // Я знаю все о пользователе!
  });

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

К сожалению, если вы не использовали return, то функция все равно вернет значение, но им будет уже не результат вызова вложенной функции, а бесполезный undefined, возвращаемый в таких случаях по-умолчанию.

Для себя я ввел правило, которое затем переросло в привычку — всегда использовать return внутри then или выдавать ошибку при помощи throw. Я рекомендую вам поступать так же.

3. Выдавать синхронную ошибку


Вот мы и подошли к throw. Здесь промисы начинают сиять еще ярче. Предположим, мы хотим выдать (throw) синхронную ошибку, если пользователь не авторизован. Это просто:

getUserByName('nolan').then(function (user) {
    if (user.isLoggedOut()) {
      // Пользователь вышел — выдаем ошибку!
      throw new Error('user logged out!');
    }
    if (inMemoryCache[user.id]) {
      // Данные этого пользователя уже есть,
      // возвращаем сразу
      return inMemoryCache[user.id];
    }
    // А вот про этого пока не знаем,
    // вернем промис запроса
    return getUserAccountById(user.id);
  })
  .then(function (userAccount) {
    // Я знаю все о пользователе!
  })
  .catch(function (err) {
    // Упс, ошибка, но мы к ней готовы!
  });

Наш catch() получит синхронную ошибку, если пользователь не авторизован, или асинхронную, если любой из промисов выше перейдет в состояние rejected. И снова, функции в catch без разницы, была ошибка синхронной или асинхронной.

Это особенно удобно для отлавливания ошибок во время разработки. Например, формирование объекта из строки при помощи JSON.parse() где-либо внутри then() может выдать ошибку, если json невалидный. С колбэками она будет «проглочена», но при помощи метода catch() мы без труда сможем ее обработать.

Продвинутые ошибки


Хорошо, теперь, когда вы выучили главный трюк промисов, давайте поговорим о крайних случаях. Потому что крайние случаи есть всегда.

Эту категорию ошибок я называю «продвинутые», потому что встречал их только в коде хорошо знакомых с промисами программистов. Обсудить подобные ошибки мы должны для того, чтобы разобрать задачку, которую я опубликовал в самом начале статьи.

Продвинутая ошибка №1 — не знаем о Promise.resolve()


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

new Promise(function (resolve, reject) {
  resolve(someSynchronousValue);
}).then(/* ... */);

Имейте в виду, вы можете написать то же самое гораздо короче:

Promise.resolve(someSynchronousValue).then(/* ... */);

Также такой подход очень удобен для отлавливания любых синхронных ошибок. Он настолько удобен, что я использую его почти во всех методах API, возвращающих промисы:

function somePromiseAPI() {
  return Promise.resolve()
    .then(function () {
      doSomethingThatMayThrow();
      return 'foo';
    })
    .then(/* ... */);
}

Просто запомните, любой код, который может выдать синхронную ошибку — потенциальная проблема при отладке из-за «проглоченных» ошибок. Но если вы обернете его в Promise.resolve(), то можете быть уверены, что поймаете ее при помощи catch().

Еще есть Promise.reject(). Его можно использовать для возвращения промиса в статусе rejected:

Promise.reject(new Error(‘Какая-то ужасная ошибка’));

Продвинутая ошибка №2 — catch() не одно и то же с then(null, …)


Чуть выше я упоминал, что catch() — это просто «сахар». Два примера ниже эквивалентны:

somePromise().catch(function (err) {
  // Обрабатываем ошибку
});

somePromise().then(null, function (err) {
  // Обрабатываем ошибку
});

Однако, примеры ниже уже не «равны»:

somePromise().then(function () {
    return someOtherPromise();
  })
  .catch(function (err) {
    // Обработка ошибка
  });

somePromise().then(function () {
  return someOtherPromise();
}, function (err) {
  // Обработка ошибки
});

Если вы задумались, почему примеры выше «не равны», посмотрите внимательно, что произойдет, если в первой функции возникнет ошибка:

somePromise().then(function () {
    throw new Error('oh noes');
  })
  .catch(function (err) {
    // Ошибка поймана! :)
  });

somePromise().then(function () {
  throw new Error('oh noes');
}, function (err) {
  // Ошибка? Какая ошибка? O_o
});

Получается, что, если вы используете формат then(resolveHandler, rejectHandler), то rejectHandler по факту не может поймать ошибку, возникшую внутри функции resolveHandler.

Зная эту особенность, для себя я ввел правило никогда не использовать вторую функцию в методе then(), а взамен всегда добавлять обработку ошибок ниже в виде catch(). Исключение у меня только одно — асинхронные тесты в Mocha, в случаях, когда я намеренно жду ошибку:

it('should throw an error', function () {
  return doSomethingThatThrows().then(function () {
    throw new Error('I expected an error!');
  }, function (err) {
    should.exist(err);
  });
});

К слову, Mocha и Chai — отличная комбинация для тестирования основанного на промисах API.

Продвинутая ошибка №3 — промисы против фабрик промисов


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

Сгоряча вы можете написать что-то подобное:

function executeSequentially(promises) {
  var result = Promise.resolve();
  promises.forEach(function (promise) {
    result = result.then(promise);
  });
  return result;
}

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

Причина в том, что по спецификации промис начинает выполнять заложенную в него логику сразу после создания. Он не будет ждать. Таким образом, не сами промисы, а массив фабрик промисов — это то, что действительно нужно передать в executeSequentially:

function executeSequentially(promiseFactories) {
  var result = Promise.resolve();
  promiseFactories.forEach(function (promiseFactory) {
    result = result.then(promiseFactory);
  });
  return result;
}

Я знаю, вы сейчас думаете: «Кто, черт возьми, этот Java программист, и почему он рассказывает нам о фабриках?». На самом деле фабрика — это простая функция, возвращающая промис:

function myPromiseFactory() {
  return somethingThatCreatesAPromise();
}

Почему этот пример будет работать? А потому, что наша фабрика не создаст промис до тех пор, пока до него не дойдет очередь. Она работает именно как resolveHandler для then().

Посмотрите внимательно на функцию executeSequentially() и мысленно замените ссылку на promiseFactory ее содержимым — сейчас над вашей головой должна радостно вспыхнуть лампочка :)

Продвинутая ошибка №4 — что, если я хочу результат двух промисов?


Часто бывает, что один промис зависит от другого, а нам на выходе нужны результаты обоих. Например:

getUserByName('nolan').then(function (user) {
    return getUserAccountById(user.id);
  })
  .then(function (userAccount) {
    // Стойте, мне еще и объект «user» нужен!
  });

Желая оставаться хорошими JavaScript разработчиками, мы возможно захотим вынести на более высокий уровень видимости переменную user, дабы не создавать «пирамиду зла».

var user;
getUserByName('nolan').then(function (result) {
    user = result;
    return getUserAccountById(user.id);
  })
  .then(function (userAccount) {
    // Хорошо, теперь есть и «user», и «userAccount»
  });

Это работает, но лично я считаю, что код «попахивает». Мое решение — отодвинуть в сторону предубеждения и сделать осознанный шаг в сторону «пирамиды»:

getUserByName('nolan').then(function (user) {
    return getUserAccountById(user.id)
      .then(function (userAccount) {
        // Хорошо, теперь есть и «user», и «userAccount»
      });
});

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

function onGetUserAndUserAccount(user, userAccount) {
  return doSomething(user, userAccount);
}

function onGetUser(user) {
  return getUserAccountById(user.id)
    .then(function (userAccount) {
      return onGetUserAndUserAccount(user, userAccount);
    });
}

getUserByName('nolan')
  .then(onGetUser)
  .then(function () {
    // К этому моменту функция doSomething() выполнилась,
    // а отступы не выросли — пирамидой и не пахнет
  });

По мере того, как код будет усложняться, вы обратите внимание, что все большая его часть преобразовывается в именованные функции, а сама логика приложения начинает приобретать вид, приносящий эстетическое удовольствие:

putYourRightFootIn()
  .then(putYourRightFootOut)
  .then(putYourRightFootIn)
  .then(shakeItAllAbout);

Вот для чего нам промисы.

Продвинутая ошибка №5 — «проваливание» сквозь промисы


Наконец, именно на эту ошибку я намекал, предлагая вам решить задачку в начале статьи. Это очень редкий случай. Возможно, он никогда не появится в вашем коде. Повстречав его, я определенно удивился.

Как вы думаете, что выведет в консоль этот код?

Promise.resolve('foo')
  .then(Promise.resolve('bar'))
  .then(function (result) {
    console.log(result);
  });

Если вы думаете, что это будет bar, вы ошиблись. В консоли появится foo!

Причина в том, что, когда вы передаете в then() что-то отличное от функции (например, промис), это интерпретируется как then(null) и в следующий по цепочке промис «проваливается» результат предыдущего. Проверьте сами:

Promise.resolve('foo')
  .then(null)
  .then(function (result) {
    console.log(result);
  });

Добавьте сколько угодно then(null), результат останется прежним — в консоли вы увидите foo.

Данный пример возвращает нас к выбору между промисами и фабриками промисов. Мы разбирали его выше. Если коротко, вы можете передавать прямо в then() промис, но результат будет совсем не тем, что вы ожидали. Метод then() ожидает функцию. Чтобы ожидания сбылись, нужно переписать пример как-то так:

Promise.resolve('foo')
  .then(function () {
    return Promise.resolve('bar');
  })
  .then(function (result) {
    console.log(result);
  });

В консоли вы увидите bar, как и ожидали.

Запоминаем: в метод then() передаем только функции.

Решение задачи


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

Вот ответы на все пазлы с визуализацией для лучшего понимания.

Пазл №1


doSomething().then(function () {
    return doSomethingElse();
  })
  .then(finalHandler);

Ответ:

doSomething
|-----------------|
                  doSomethingElse(undefined)
                  |------------------|
                                     finalHandler(resultOfDoSomethingElse)
                                     |------------------|

Пазл №2


doSomething().then(function () {
    doSomethingElse();
  })
  .then(finalHandler);

Ответ:

doSomething
|-----------------|
                  doSomethingElse(undefined)
                  |------------------|
                  finalHandler(undefined)
                  |------------------|

Пазл №3


doSomething().then(doSomethingElse())
  .then(finalHandler);

Ответ:

doSomething
|-----------------|
doSomethingElse(undefined)
|---------------------------------|
                  finalHandler(resultOfDoSomething)
                  |------------------|

Пазл №4


doSomething().then(doSomethingElse)
  .then(finalHandler);

Ответ:

doSomething
|-----------------|
                  doSomethingElse(resultOfDoSomething)
                  |------------------|
                                     finalHandler(resultOfDoSomethingElse)
                                     |------------------|

Если вы не поняли объяснения, то я советую вам перечитать статью еще раз, или самостоятельно написать функции doSomething() и doSomethingElse(), и поэкспериментировать с ними в браузере. Подразумевается, что эти функции возвращают промисы с какими-то результирующими данными.

Также обратите внимание на мой список полезных заготовок.

Заключительное слово о промисах


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

Если вы мне не верите, держите пруф-линк — рефакторинг PounchDB модуля map/reduce с заменой колбэков на промисы. Результат: 290 добавленных строк, 555 удаленных. По-случайности, автором всех этих прежних жутких колбэков был… я. Так что это стало для меня первым из освоенных преимуществ промисов.

Я уже говорил, промисы великолепны. Это правда, что они лучше колбэков. Тем не менее, это все равно, что выбирать между пинком в живот и ударом в зубы. Да, что-то из этого лучше, но еще лучше избежать обоих вариантов. Промисы все еще трудны для понимания, и можно попасть в ситуацию, когда результат работы кода намного отличается от задуманного. Даже опытные разработчики могут попасть в коварную ловушку. Основная проблема в том, что промисы, хоть и реализуют паттерны, схожие с синхронным кодом, на деле совсем не равны им.

Ждем async/await


В своей статье «Укрощение асинхронного чудовища с ES7» я упоминал async/await и то, как они глубже интегрировали промисы в язык. Вместо написания псевдо-синхронного кода (с фейковым методом catch(), который как из try/catch, но не совсем, ES7 позволит нам использовать настоящие try/catch/return.

Это огромное благо для JavaScript, как для языка, потому что анти-паттерны использования промисов по-прежнему будут возникать, пока наши инструменты не будут сообщать нам о сделанных ошибках.

В качестве примера из истории JavaScript, я думаю, что JSLint и JSHint сделали для сообщества гораздо больше, чем «Хорошие стороны JavaScript», хоть в целом суть у них похожа. Разница в том, что в первом случае вам говорят о конкретной ошибке, которую вы сделали в определенной строке, а во втором вы читаете большую книгу о том, какие ошибки делают другие разработчики.

Красота async/await в основном в том, что ваши ошибки проявят себя сразу при синтаксическом анализе в интерпретаторе JS, а не когда-то возможно где-то во время исполнения кода. До тех же пор полезным будет представление о возможностях промисов, о том, как их использовать в ES5 и ES6.

Как и книга «Хорошие стороны JavaScript», эта статья имеет малый эффект. Вероятно однажды вы сможете дать на нее ссылку своему коллеге, который найдет в себе силы честно признать:

— У меня проблемы с промисами!

Комментарии (119)


  1. Zveroloff
    26.10.2015 10:00
    +19

    Чего-то я лох, по-моему…


  1. kwolfy
    26.10.2015 10:18
    +3

    За Promise.resolve() спасибо


  1. faiwer
    26.10.2015 10:24
    +9

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

    Продвинутая ошибка №5 — «проваливание» сквозь промисы
    А вот этого я не знал. Не ожидал такого поведения. Даже странно, как это я ещё не напоролся на это через какой-нибудь баг. Правда не совсем понятно, зачем они так сделали?!

    Ждем async/await
    А пока ждём, на серверной стороне, можно использовать связку co + generator-ы или ей подобную. Получаем практически тоже самое, с практически таким же синтаксисом. Причём без транспиляторов.


    1. hell0w0rd
      26.10.2015 13:53
      +3

      co — медленная библиотека. Используйте bluebird.coroutine.


      1. Santacruz
        27.10.2015 11:03

        Опа… а catch есть? в описании не видно


        1. hell0w0rd
          27.10.2015 17:26

          catch?


  1. k12th
    26.10.2015 11:22

    Также такой подход очень удобен для отлавливания любых синхронных ошибок. Он настолько удобен, что я использую его почти во всех методах API, возвращающих промисы:
    <...>
    Просто запомните, любой код, который может выдать синхронную ошибку — потенциальная проблема при отладке из-за «проглоченных» ошибок. Но если вы обернете его в Promise.resolve(), то можете быть уверены, что поймаете ее при помощи catch().

    Ну-ну. Больше коллбэков для бога коллбэков!


  1. aenesterov
    26.10.2015 11:47
    +8

    Здесь можно посмотреть визуализацию работы promise, может быть полезна для лучшего их понимая.


  1. Lisio
    26.10.2015 12:04
    +2

    Могу ошибиться, но в примере к ошибке №1 получившийся код хоть и выглядит красивее, но работать не будет. По крайней мере, в исходной задаче видно, что put делается для каждого объекта из массива результатов, а в отрефакторенной — один put на весь массив.


    1. mayorovp
      26.10.2015 12:30

      Ну, этот момент потом в примере номер 2 разобран


      1. Lisio
        26.10.2015 13:09
        +1

        Частично, но это не делает первый пример рабочим. Не покидает ощущение, что код просто подогнали так, чтобы с первого взгляда выглядел красивым. Если применить технику из второго примера на все шаги из первого, то картина может получиться сильно хуже.


  1. musuk
    26.10.2015 12:54
    +1

    После того, как в node.js появилась нормальная поддержка генераторов без специальных ключей можно просто пользоваться bluebird Promise.coroutine и получать красивый асинхронный код с нормальными try… catch… и ветвлениями и отменой операций без боли.

    Почувствуй разницу между:

    doSomething().then(function () {
      return doSomethingElse();
    });
    


    и

    Promise.coroutine(function*(){
        yield doSomething();
        return yield doSomethingElse();
    })();
    


    1. Fesor
      26.10.2015 13:04

      а с async/await (которые вроде как уже заимплеменчены в babel на этой основе) это еще и читабельным будет.


      1. VasilioRuzanni
        26.10.2015 16:28

        Так они недавно (в сентябре) еще и перешли в статус Stage 3 (Candidate).


    1. KlonD90
      26.10.2015 15:34
      +4

      Эти генераторы не такие уж веселые на вид. Вы реально их используете в продакшене?


      1. musuk
        26.10.2015 15:44
        +2

        Что в них невесёлого, когда есть Promise.coroutine?


        1. VasilioRuzanni
          26.10.2015 16:38
          +2

          В этом плане я совершенно согласен с Домеником Деникола, что генераторы для «синхронности в асинхронном коде» — это больше хак и workaround, они изначально задумывались для других целей. И async/await — именно для управления асинхронным потоком исполнения в приложении, а генераторы останутся для того, для чего были задуманы изначально (итерация, удобные ко-рутины).

          image


          1. sompylasar
            31.10.2015 00:32

            Интересно, что MS в своем движке браузера Chakra реализовали async/await как раз при помощи генераторов и yield:
            habrahabr.ru/company/microsoft/blog/269871


  1. tyomitch
    26.10.2015 17:18
    +2

    Что за подвох? Откуда в решениях задачи finalHandler, если в условии его не было?


    1. drfisher
      26.10.2015 17:24
      +1

      Думаю, автор имел в виду конструкцию /*...*/ из оригинального твита. Спасибо за наблюдательность!


  1. Imira_crai
    26.10.2015 17:42
    +2

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


  1. SamVimes
    26.10.2015 18:12
    +2

    Мне кажется, или для пазла 2 ошибка в ответе? (и соответственно для всего, где похожие ситуации):

    doSomething
    |-----------------|
                      doSomethingElse(undefined)
                      |------------------|
                      finalHandler(undefined)
                      |------------------|
    

    finalHandler вызовется после того, как doSomethingElse закончит работу. Возможно, автор имел в виду, что если в then вернётся promise, то в then для then будет результат этого промиса?


    1. drfisher
      26.10.2015 18:22
      -1

      Если бы перед doSomethingElse стоял return, то да, finalHandler ждал бы выполнения промиса из doSomethingElse. В нашем же случае мы имеем дело со «сторонним эффектом», где doSomethingElse выполняется, возможно даже возвращает промис, вот только использовать в finalHandler мы его не сможем.


      1. SamVimes
        26.10.2015 18:24
        +1

        Да, я это понимаю. Просто «схема» может сбить с толку тех, кто плохо знаком с промисами. Она ведь не совсем так должна выглядеть. doSomethingElse выполнится в любом случае до finalHandler, мы не знаем что он возвращает, да нас это и не интересует:)


        1. drfisher
          26.10.2015 18:35

          Здесь скорей имеется в виду, что начнут они свое выполнение одновременно (почти). При этом внутри функции doSomethingElse может быть какой-нибудь долгий ajax-запрос, результат которого придет только через несколько секунд (предположим, что интернет барахлит), а значит закончит свою работу она значительно позже.
          Если бы перед вызовом doSomethingElse стоял return, то finalHandler честно бы ждал результат ajax-запроса.


          1. SamVimes
            26.10.2015 18:44
            +2

            Но почему почти? У нас всё в одном потоке тут, вызовы идут подряд. То что return promise будет ждать выполнения этого самого промиса это просто то, как обрабатывается результат then.

            .then(f1).then(f2)
            

            Функция f1, которую мы в then передаём выполниться раньше чем f2.


            1. SamVimes
              26.10.2015 22:52
              +2

              Ну вот нифига себе, я правильно пишу, а мне минусы ставят. Как же так :( Хоть бы причину писали


          1. defuz
            26.10.2015 23:52
            +1

            Начнут выполнение одновременно? В javascript? Вы серьезно?


          1. defuz
            27.10.2015 00:14

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

            Здесь скорей имеется в виду, что начнут они свое выполнение одновременно (почти).
            Движок javascript выполнеяется в однопоточном eventloop, так что никакие две нормальные функции вообще никогда не могут выполнятся одновременно.
            При этом внутри функции doSomethingElse может быть какой-нибудь долгий ajax-запрос, результат которого придет только через несколько секунд (предположим, что интернет барахлит), а значит закончит свою работу она значительно позже.
            Функция, которая инициировала ajax-запрос всегда выполняется раньше, чем будет завершен этот ajax-запрос (и будет вызван его обработчик).
            Если бы перед вызовом doSomethingElse стоял return, то finalHandler честно бы ждал результат ajax-запроса.
            Нет, потому что сама функция doSomethingElse может не возвращать promise этого ajax-запроса.


            1. drfisher
              27.10.2015 08:59
              +1

              Под «одновременно (почти)» я имел в виду не «параллельно», а «сразу, без ожидания». Я говорил не о том, когда функции начнут и завершат свою работу буквально. Я имел в виду полное завершение их внутренних задач, в асинхронной логике это получение результата.
              Простите, что смутил вас и спасибо за ваше уточнение!


        1. mayorovp
          26.10.2015 18:50
          +1

          На этих схемах показан не синхронный поток выполнения, а асинхронный: под doSomethingElse(undefined) понимается не только сам вызов функции, но и завершение того промиса, который эта функция вернула.

          Вот, кстати, еще одна проблема с промисами, неназванная в статье. Непонимание того факта, что как только в программу вводятся элементы асинхронности, потоки управления начинают выглядеть по-разному в зависимости от того, с синхронной или асинхронной точек зрения мы их рассматриваем.


          1. SamVimes
            26.10.2015 18:59
            +1

            Такой ответ мне нравится. Но, лучше на схеме указывать, что имеется в виду:-) А то гадать какая тут у автора точка зрения, это не очень круто.


  1. DmitryO
    26.10.2015 19:05
    -8

    Я понимаю, что в современном мире можно подогнать любую тему под практически любую технологию, и статья хороша, но…

    Что этот перевод делает в блоге компании Mail.ru?


  1. Blumfontein
    26.10.2015 21:06
    -1

    Эммм, может я что-то не понимаю, откуда finalHandler из примера 2 знает, возвращает function() {doSomethingElse()} что-то или не возвращает? Это в примере все довольно просто, а на практике там может быть 10-этажный switch или if/else, и там в зависимости от условий может возвращаться значение, а может и не возвращаться… Ну т.е. then(finalHandler) должен дождаться выполнения предыдущего then для того, чтобы понять, возвращает или не возвращает что-то он.


    1. drfisher
      26.10.2015 21:15

      В примере 2 finalHandler не знает, что возвращает функция doSomethingElse. Мало того, она даже не знает, что doSomethingElse существует, потому что результат ее работы улетает «в пустоту». В примере 2 finalHandler сразу получает на вход undefined (значение, возвращаемое любой функцией по умолчанию).

      Возникает вопрос, как можно пробросить результат работы doSomethingElse в finalHandler? Ответ простой — добавить перед ее вызовом return:

      doSomething().then(function () {
          doSomethingElse();
        })
        .then(finalHandler); // undefind
      
      doSomething().then(function () {
          return doSomethingElse(); // "возвращаем" результат работы вложенной функции
        })
        .then(finalHandler); // тут finalHandler получит на вход результат doSomethingElse
        // и, если это промис, то дождется его "выполнения", а значит и результат его выполнения
      


      1. Blumfontein
        26.10.2015 21:25

        Хорошо, что получит finalHandler в следующем случае?

        doSomething().then(function () {
        if (my_var == 2) {
        return doSomethingElse();
        } else {
        doSomethingElse();
        }
        })
        .then(finalHandler);


        1. drfisher
          26.10.2015 21:30

          Либо результат работы doSomethingElse, либо undefined. Все зависит от переменной my_var.


          1. Blumfontein
            26.10.2015 21:38

            А все, я понял, похоже затупил


  1. js605451
    26.10.2015 22:01

    А кто расскажет:

    Вот вариант получше:
    remotedb.allDocs(...)
      .then(function (resultOfAllDocs) {
        return localdb.put(...);
      })
      .then(function (resultOfPut) {
        return localdb.get(...);
      })
      .then(function (resultOfGet) {
        return localdb.put(...);
      })
      .catch(function (err) {
        console.log(err);
      });
    


    как в этом замечательном примере:
    1. Добавить _разную_ обработку ошибок для localdb.put() и localdb.get()?
    2. Внутри третьего блока then получить доступ к resultOfAllDocs и resultOfPut?


    1. mayorovp
      26.10.2015 22:10
      +5

      function appendArgs() {
          var args1 = arguments;
          return function() {
              return Array.prototype.concat.call(args1, arguments);
          }
      }
      
      remotedb.allDocs(...)
        .then(function (resultOfAllDocs) {
          return localdb.put(...)
            .then(appendArgs(resultOfAllDocs), function(err) { /*Обработка ошибки 1*/ })
        })
        .then(function (resultOfAllDocs, resultOfPut) {
          return localdb.get(...)
            .then(appendArgs(resultOfAllDocs, resultOfPut), function(err) { /*Обработка ошибки 2*/ })
        })
        .then(function (resultOfAllDocs, resultOfPut, resultOfGet) {
          return localdb.put(...)
            .then(appendArgs(resultOfAllDocs, resultOfPut, resultOfGet), function(err) { /*Обработка ошибки 3*/ })
        })
        .then(function (resultOfAllDocs, resultOfPut, resultOfGet, resultOfPut2) {
          /* Какая-нибудь окончательная обработка результатов */
        })
        .catch(function (err) {
          console.log(err);
        });
      


    1. drfisher
      26.10.2015 22:24

      Еще вы можете использовать промежуточные catch().

      Promise.resolve('some text')
        .then(myText => {
          console.log(`My text is "${myText}"`);
          throw new Error('my error');
          return 'ololo'; // Эта строка не выполнится
        })
        .catch(errMessage => {
          console.log(`I caught an error "${errMessage}"`);
          return errMessage;
        })
        .then(someText => {
          console.log(`The message is "${someText}"`);
        });
      


  1. justboris
    26.10.2015 22:50
    +4

    В примере с продвинутой ошибкой №1 автор переусердствовал с Promise.resolve(). Непонятно, чем это

    function somePromiseAPI() {
      return Promise.resolve().then(function () {
        doSomethingThatMayThrow();
        return 'foo';
      }).then(/* ... */);
    }
    

    Лучше обычного конструктора Promise:
    function betterPromiseAPI() {
      return new Promise(function (resolve) {
        doSomethingThatMayThrow();
        resolve('foo');
      }).then(/* ... */);
    }
    

    Exception в функции, переданной в new Promise также будет перехвачен и вызовет reject. Зато во втором варианте создается на один промис меньше.


  1. mr47
    27.10.2015 00:24
    -5

    Проблема в том что не многие понимают что async/await это не панацея и стоит понимать где его использовать.

    function save(){
        return new Promise((resolve, reject)=>{
             setTimeout(()=>{
                  resolve(1);
             },300);
        })
    }
    async function test(){
         let one  = await save();
         // one = 1
         return one;
    }
    


    И вот вопрос что же вернет функция test() ?
    Promise


    1. mayorovp
      27.10.2015 06:34
      +4

      Любая асинхронная функция возвращает промис. Что в этом внезапного и неожиданного?


    1. faiwer
      27.10.2015 08:13

      А как решить такую ситуацию ?
      test().then(one => console.log(one + 2));
      

      Ну или да, выполнять test через await в рамках другой async функции.
      Я не совсем понимаю ? а на что вы рассчитывали? На то, что метод test заблочит весь поток js-а, до тех пор, пока не выполнится?


      1. mr47
        27.10.2015 13:38

        Я не совсем понимаю ? а на что вы рассчитывали? На то, что метод test заблочит весь поток js-а, до тех пор, пока не выполнится?

        Конечно же нет, но представьте как это может быть воспринято человеком только начавшим уже с es7 (c babel), все показывают красивые примеры, не вдаваясь в подробности. И ссылка на сахар за счет чего это сделано дает понять что результат будет промис.


        1. faiwer
          27.10.2015 14:02

          Ну… Любой асинхронный код это дискомфорт, в той или иной мере. К тому, что нельзя будет магическим образом писать асинхронный код синхронно я был уже готов после генераторов. Одним из неприятных моментов является то, что javascript удобно использовать в функциональном стиле, но «контекст» async-а (или generator-а) теряется в анонимных методах. Т.е. вот так вот сделать нельзя:

          async function test(list)
          {
            return list.map(el => await someAsyncMethod(el));
          }
          

          Потому что await не будет работать внутри метода. Тоже самое и в генераторах (yield). Такие библиотеки как underscore, lodash и пр. располагают к использованию цепочек методов с callback-ами. Да даже нативные методы .map, .reduce, .some и пр. тут ставят нам грабли.

          Конечно, какие-то конструкции для удобства можно написать самому, к примеру, свою универсальную реализацию .map, которой и метод и async сгодится. Но, ИМХО, проще смириться, что за любую асинхронщину нужно платить удобством.

          Очень не люблю такие вот конструкции:
          for(let i = 0, n = list.length; i < n; ++i)
          {
            var el = list[i];
          }
          

          Но «for-of» лишит меня ключа, а .each yield (async) контекста. Что ж поделать… В любом случае это гораздо удобнее, чем callback-ый ад.


          1. VasilioRuzanni
            28.10.2015 12:36
            +1

            А зачем писать свою реализацию .map() и прочих? Что мы хотим получить на выходе вот этой функции из примера?

            async function test(list) {
              return list.map(el => await someAsyncMethod(el));
            }
            


            1. Fesor
              28.10.2015 13:19
              -1

              маленькое но — в вашем примере map будет синхронным и будет дожидаться выполнения someAsyncMethod на каждой итерации.


              1. VasilioRuzanni
                28.10.2015 13:41

                Вот и мне интересно, что мы ожидаем получить из подобного рода конструкции, даже если бы она работала. Даже если ее переписать, чтобы она не использовала arrow-функцию и использовала async, что будет работать технически, то мы в итоге все равно получаем массив промисов, как если бы вообще не использовали await в коде выше.

                Я понимаю, что товарищ faiwer говорил про «контекст», и ведь вроде как в стрелочных функциях тот же «this», что и за ее пределами, так почему бы не передавать и «async» — но это другое. Как по мне — несмотря на необходимость явно отмечать функции как «async» — код получается с виду очень даже синхронным (но на практике надо понимать, конечно, во что он разворачивается).


                1. faiwer
                  28.10.2015 13:47

                  Даже если ее переписать, чтобы она не использовала arrow-функцию и использовала async, что будет работать технически, то мы в итоге все равно получаем массив промисов, как если бы вообще не использовали await в коде выше.

                  Гхм. Почему «массив промисов» а не массив результатов работы промисов?

                  Я понимаю, что товарищ faiwer говорил про «контекст», и ведь вроде как в стрелочных функциях тот же «this», что и за ее пределами

                  Прошу прощения, что запутал вас. Я вовсе не имел ввиду ни this, ни scope. Слово «контекст» я использовал в более широком значении. Имею ввиду то, что доступные возможности в рамках «тела» обычного function-а, generator-а и async function-а разные. В первом случае мы не можем применять ни await, ни yield; во втором только yield; а в третьем только await; Каждая стрелочная или обычная анонимная функция такой «контекст» прекращает, руководствуясь уже возможностями обычных функций. А так как async-функции и generator-ы это, по большому счёту, вообще не функции, то применять их в качестве обычных callback-ов мы не можем.

                  Надеюсь теперь стало яснее, что я имею ввиду :)


                  1. VasilioRuzanni
                    28.10.2015 14:07

                    Ну, тут как ни пиши, результат работы самой async функции будет промисом, а .map() — синхронный. В случае

                    async function test(list) {
                      var result = list.map(async function (el) {
                        return await someAsyncMethod(el);
                      });
                      console.log(result);
                      return result;
                    }
                    

                    в консоль выводятся объекты промисов, а в случае
                    async function test(list) {
                      var result = await* list.map(el => someAsyncMethod(el));
                      console.log(result);
                      return result;
                    }
                    

                    результаты работы промисов.


                    1. faiwer
                      28.10.2015 14:12

                      теперь понял вас. Логично, что затолкав в .map async-и мы получим промисы :) Но я имел ввиду что-то похожее на вот это:

                      return await list.asyncMap(someAsyncMethod);

                      Где asyncMap сам async-метод, который await-ми переберёт list someAsyncMethod-ом.

                      Но даже написав свой .asyncMap мы толком ничего не выиграем. Потому, что для того чтобы цепочка из вот таких вот async-монстро-функций заработала, придётся изрядно по… возиться. И всё равно получится что попало.


                      1. VasilioRuzanni
                        28.10.2015 14:42

                        Ну, в точности, как синхронно, конечно, не будет. Но с «await*», который, как говорят ниже, убрали из спека и с Promise.all получаем вполне работающий синхронный .map, но оперировать все равно придется промисами — впрочем, это в любом случае. .asyncMap вполне несложно реализовать, использовав внутри .map и Promise.all — так что тут я особой монструозности то и не вижу. Единственное — всегда нужно держать в голове, что там происходит за кулисами.


                        1. faiwer
                          28.10.2015 14:54

                          так что тут я особой монструозности то и не вижу

                          return await list.asyncMap(fn1)
                                 .asyncFilter(fn2)
                                 .reduce(fn3)
                                 .some(fn4);
                          

                          А так? :)


                          1. VasilioRuzanni
                            28.10.2015 15:08

                            Чем-то жертвовать придется :)

                            Если filter тоже должен быть асинхронным — то сложнее, но если мы после .asyncMap работаем чисто с результатами промисов — то можно использовать синхронно все остальное.

                            var items = await list.asyncMap(fn1);
                            return items
                                   .filter(fn2)
                                   .reduce(fn3)
                                   .some(fn4);
                            


                            1. faiwer
                              28.10.2015 15:11

                              Чем-то жертвовать придется :)
                              Ага. Собственно об этом я и говорю. Так то да, await, async, yield это куда лучше, чем calback-hell или promise-hell :)


              1. faiwer
                28.10.2015 13:42

                Ну собственно именно такое поведение и ожидается. Но на самом деле будет syntax error. Потому что await работает только в контексте async-function, а стрелочные функции — async-ами не являются.


            1. faiwer
              28.10.2015 13:39

              А зачем писать свою реализацию .map() и прочих? Что мы хотим получить на выходе вот этой функции из примера?

              Ну функция в примере сделала бы следующее (на самом деле будет syntax error):

              async function test(list)
              {
                var result = [];
                for(let el of list)
                {
                  result.push(await someAsyncMethod(el));
                }
                return result;
              }
              

              Что делает someAsyncMethod не важно. Важно что он возвращает что-либо полученное асинхронно. В примере выше код стал более громоздким и менее читаемым. Функциональный подход часто бывает более наглядным.

              Но пока у нас 1 map, нам по большому счёту плевать. А что если у нас типовая lodash цепочка методов? К примеру из 8 последовательных chain-вызовов? Придётся забить на lodash цепочку и написать всё в обычном стиле, через for-ы, if-ы и пр… А это, зачастую, бывает очень неудобным, в особенности после того как подсел на функциональный подход.

              Т.е. получается что async-и и generator-ы НЕ позволяют писать асинхронный код также удобно, как и синхронный. И дело не только в том, что при более менее сложной логике async-и придётся плодить везде (что местами создаст определённые неудобства, потребует обёрток и пр. шаманств). А в том, что ограничения куда шире.

              Но в любом случае, через async-ки и generator-ы писать сложный асинхронный гораздо удобнее.


              1. VasilioRuzanni
                28.10.2015 13:48

                Как по мне — вполне нормально можно сделать и с async/await:

                async function test(list) {
                  return await* list.map(el => someAsyncMethod(el));
                }
                


                1. rock
                  28.10.2015 13:54
                  +1

                  await* давно удалён из предложения и, например, в babel 6 будет выпилен. Замените на Promise.all.


                  1. VasilioRuzanni
                    28.10.2015 14:43

                    А есть где-то информация почему так? Все-таки Promise.all — это дополнительный шум в коде. Как-то я упустил этот момент с удалением «await*».


                    1. rock
                      28.10.2015 15:13
                      +1

                      Ну я какбэ тоже не в восторге, из обсуждения комитетом, можете поискать ещё информации в ишьюшках.


                    1. rock
                      28.10.2015 15:38

                      Кстати, асинхронные стрелки тоже могут быть удалены (хотя, думаю, маловероятно)


                1. faiwer
                  28.10.2015 13:54
                  -2

                  Пока код состоит из одной строки — да.


              1. faiwer
                28.10.2015 14:18

                Ещё небольшой пример. Наверняка многие уже взяли за привычку использовать .forEach или _.each, для перебора массивов. Это удобнее, чем for-of потому, что мы не теряем индексы. В случае async-метода приходится использовать

                for(let i = 0, n = list.length; i < n; ++ i)
                {
                  let el = list[i];
                  // ...
                }


                Правда тут есть 1 решение. Дождаться пока node (я не использую babel) сможет вот так:

                for(let [el, i] from iter(list))

                Где iter будет костылём, который позволит перебирать массив массив на манер Map-а.

                Честно говоря, я не слежу за ES6-7. В Array случаем уже не добавили возможность перебора for-of-м без потери индексов?


                1. rock
                  28.10.2015 14:28
                  +2

                  for(let [key, val] of ['a', 'b', 'c'].entries())console.log(key, val);
                  

                  давно часть ES6 и никакие индексы не теряются. Ну а то, что V8 пока не поддерживает деструкцию — его проблемы — никто не мешает использовать транспайлер.


                  1. faiwer
                    28.10.2015 14:33

                    Спасибо. Возьму на заметку.


                1. mayorovp
                  28.10.2015 14:53

                  .forEach или _.each прекрасно заменяются на map. А что делать с map — мы уже знаем.


          1. mayorovp
            28.10.2015 13:45

            function test(list)
            {
              return Promise.all(list.map(el => someAsyncMethod(el)));
            }
            


            Ну или если нужен обязательно асинхронный код, то так:
            async function test(list)
            {
              return await Promise.all(list.map(el => someAsyncMethod(el)));
            }
            


            1. faiwer
              28.10.2015 13:52

              mayorov невероятно, оказывается существует Promise.all. Фантастика. Это всё меняет.

              Прошу читать мои комментарии не по диагонали. Что вы будете делать в случае underscore, lodash или какой-бы то ни было ещё цепочке, основанной не на промисах? Если у вас каррирования, монады или ещё что-нибудь в таком духе?

              Скорее всего забьёте и перепишите на не-функциональный подход. О чём я и говорю.


              1. mayorovp
                28.10.2015 14:50

                А какие еще функции имеют смысл в асинхронном варианте? Просмотрел список функций того же lodash — и не нашел куда бы я мог передать лямбду, возвращающую промис кроме как в map.

                А что делать с map я уже писал.


                1. mayorovp
                  28.10.2015 15:02

                  UPD: ах да, filter, как я мог забыть. Для него я напишу свою реализацию. Один раз.


                  1. faiwer
                    28.10.2015 15:06

                    В идеале как то так:

                    return await list
                          .filter(fn1)
                          .map(fn2)
                          //
                          .invoke(fn3)
                          .reduce(fn4)
                          .groupBy(fn5)
                          .map(fn6)
                          .value();
                    

                    Получится весьма не тривиально :) Хотя да, более, чем подъёмно.


                1. faiwer
                  28.10.2015 15:04

                  А какие еще функции имеют смысл в асинхронном варианте?
                  Любые из тех, что принимают callback-и. А это половина библиотеки. Из самых часто используемых: .flatten (либо .flatMap), .reduce, .filter, .transform, .each. К примеру, в .filter может потребоваться некая проверка значения memcahed по ключу.

                  К тому же стоит учесть, что очень часто они применяются в цепочке, а не раздельно друг от друга.

                  А ещё вы ошибаетесь приравнивания .map, .each и Promise.all. Как минимум некоторые реализации .each-а умеют break-ать при return false.


  1. elser
    27.10.2015 03:15
    +4

    сделайте то, что JavaScript разработчики делали испокон веков — создайте функцию и используйте ее по имени

    вот, золотые слова.
    Да, не спорю, промисы — это красиво и автор абсолютно прав, что думать о них, как о серебряной пуле для борьбы с callback hell не совсем верно, так как по сути, от пирамиды зла можно избавиться именно так, как сказано в этой фразе — без специальных конструкций и библиотек для работы с асинхронной логикой.

    на пример вот это:
    doAsync1(function () {
      doAsync2(function () {
        doAsync3(function () {
          doAsync4(function () {
             // ...
          })
        })
      })
    })
    

    можно и так сделать плоским:
    var callbacks = {
      async1CallBack: function(){
        doAsync2(callbacks.async2CallBack);
      },
      async2CallBack: function(){
        doAsync3(callbacks.async3CallBack);
      },
      async3CallBack: function(){
        doAsync4(callbacks.async4CallBack);
      },
      async4CallBack: function(){
        // ...
      }
    }
    doAsync1(callbacks.async1CallBack);
    

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

    doAsync1()
      .then(doAsync2)
      .then(doAsync3)
      .then(doAsync4);
    

    Вот для чего нам промисы.


  1. BoltThrower
    27.10.2015 11:56
    -5

    97 употреблений «промисов» в статье, давно меня так не корёжило. Не имел дела с яваскриптом, там что, правда прижился такой термин?


    1. Fesor
      27.10.2015 12:07

      1. BoltThrower
        27.10.2015 12:23
        -3

        Нет, я не сомневаюсь, что в яваскрипте есть нечто, по-английски называемое «promise», но в статье-«переводе» употреблять такую кальку как-то уж совсем неприлично. Это перевод с латиницы на кириллицу, а не с английского на русский.


        1. Fesor
          27.10.2015 12:29

          «обещание» не очень хорошо звучит, отложенные объекы — лучше конечно но это чуть чуть другое.


          1. BoltThrower
            27.10.2015 12:44

            Ну англоязычные пользователи используют же как-то «обещания», а не какие-нибудь obeschaniya, а у нас почему-то любят оперировать терминами, вместо того чтобы оперировать смыслами.


          1. faiwer
            27.10.2015 14:04

            «обещание» не очень хорошо звучит, отложенные объекы — лучше конечно но это чуть чуть другое.
            Многократно встречал в сети promise-ы именно как «обещания». Можно сказать — уже привык к такому термину. Слух не корёжит. ИМХО, термин вполне подходящий.


    1. drfisher
      27.10.2015 12:27
      +3

      Да, термин прижился именно так.


  1. interist32
    27.10.2015 12:51
    +1

    Отличный перевод. Автору большое спасибо. Несколько раз порывался прочесть данную статью в оригинале и откладывал на потом. Сейчас прочел на одном дыхании. Проблема, к сожалению, как мне кажется в том, что программисты, особенно начинающие, не читают спецификаций, а основываются на Stackoverflow-советах и примерах. Есть отличная серия докладов Domenic Denicola про промисы. Всем советую.


  1. fetis26
    27.10.2015 13:22
    +1

    Читал ее в оригинал. Отлично что сделали перевод на русский. Статься шикарная.


  1. fetis26
    27.10.2015 13:41

    И такой вот вопрос у меня вчера возник

    Правильно ли я понимаю, что если один из промисов в цепочке сделал reject, то вся цепочка прерывается и никакого значения наружу из обработчика reject мы не вернем. У меня была задача в случае reject одного промиса, вернуть другой.


    1. mayorovp
      27.10.2015 13:53
      +2

      Для этих целей и существует catch. Он тоже может возвращать результат или даже промис.

      (Только осторожнее с JQuery — в 1 и 2й версиях там catch работает по-другому)


  1. sompylasar
    28.10.2015 00:36
    +1

    Перевод отличный!

    Тем, кто пользуется jQuery.Deferred, необходимо помнить, что он не следует пункту 2.2.4 спецификации Promises/A+: функции выполняются по возможности синхронно, в текущем стеке выполнения, и throw внутри функций, переданных в then, не приводят к reject, а прерывают текущий стек выполнения, то есть в зависимости от последовательности развития событий callback'и могут вызываться как асинхронно, так и синхронно, что очень опасно — асинхронные функции могут внезапно стать синхронными.

    Здесь хорошо описана разница между синхронными и асинхронными callback'ами, и делается упор на то, что, предоставляя интерфейс с callback'ами, необходимо делать их либо всегда синхронными, либо всегда асинхронными, но не смешивать.

    С другими реализациями, которые следуют всем требованиям Promises/A+, promise поменяет свое состояние только после завершения всех выполняющихся функций, то есть стек при смене состояния promise и вызове callback'ов всегда будет «пустой» (содержать только вызовы функций «платформы» и не содержать вызовов функций приложения).

    Например,

    function getSomethingPromised() {
        var def = jQuery.Deferred();
    
        def.then(function () {
            throw new Error();
        });
    
        def.resolve();
        //< Выкинет Error отсюда, прерывая выполнение функции getSomethingPromised
        // и всех вызвавших её функций -- функция getSomethingPromised предстала как синхронная.
    
        doSomething(); //< Не выполнится!
    
        return def.promise();
    }
    


    function getSomethingPromised() {
        var def = jQuery.Deferred();
    
        def.then(function () {
            throw new Error();
        });
    
        setTimeout(function () {
            def.resolve(); //< Выкинет Error отсюда, прерывая выполнение только функции, обрабатывающей таймер.
        }, 1000);
    
        doSomething(); //< Выполнится!
    
        return def.promise(); //< Вернет promise в вызывающую функцию  -- функция getSomethingPromised предстала как асинхронная.
    }
    


    1. mayorovp
      28.10.2015 09:08

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


      1. sompylasar
        28.10.2015 23:34

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

        Например, передавая функцию в Array forEach, sort или map, я точно знаю, что моя функция будет вызвана только синхронно в процессе выполнения этих функций, и не будет больше вызываться после их завершения.

        Той же предсказуемости хотелось бы и от функций асинхронной природы (например, реализующих сетевые запросы). В jQuery гарантия асинхронного вызова callback-функций, переданных в функции асинхронной природы или подвешенных на promise, не предоставляется, что приводит к необходимости в вызывающем коде предполагать оба варианта вызова переданных функций. Это требует от разработчика большего опыта и понимания происходящего, следовательно, повышается вероятность допустить ошибку, не обработав синхронный вариант вызова callback'ов.

        Особенно подвержен ошибкам код, который меняет состояние объектов, перемежая изменения навешиванием обработчиков promise'ов. В случае синхронного вызова callback'ов из promise'ов, в зависимости от состояния primose'ов эти callback'и будут вызваны либо асинхронно, либо сразу при навешивании, и код внутри callback'ов может при некотором стечении обстоятельств получить управление раньше, чем изменение состояния объекта завершится.


  1. slavent77
    28.10.2015 19:15

    Очень полезная статья. Всегда, когда использовал Deferred, было ощущение, что это какой-то костыль.


  1. vintage
    28.10.2015 19:52

    Уже тысячу лет как изобретены корутины. Когда, конец, их внедрят в JS и перестанут изобретать всякие deffered, promises, async-await?


    1. mayorovp
      28.10.2015 20:08

      Как вы видите решение простейшей задачи — сделать 2 параллельных HTTP-запроса, дождаться их выполнения и вывести результат — на основе сопрограмм?

      А если надо вывести диалоговое окно (на html, не системное) и дождаться нажатия кнопки «ОК» — как вы будете действовать?


      1. lega
        28.10.2015 22:36

        А какие проблемы?, посмотрите на GoLang, там «все в корутинах».


        1. mayorovp
          28.10.2015 22:49

          Проблема в том, что сопрограммы сами по себе ничего не могут. Все равно нужны примитивы для асинхронности.


          1. lega
            28.10.2015 23:13

            Все равно нужны примитивы для асинхронности.
            Все io вызовы идут «асинхронно», вот пример параллельных http запросов.


            1. Fesor
              28.10.2015 23:24

              Ммм… я не вижу принципиальной разницы между использованием WaitGroup и Promise.all…

              const urls = [
                  "http://www.golang.org/",
                  "http://www.google.com/",
                  "http://www.somestupidname.com/",
              ];
              
              await Promise.all(urls.map((url) => http.get(url)));
              


              Вообще горутины это чуть больше чем корутины… это корутины которые еще и по тредам распределяются автоматически в рантайме. Вот в python корутины есть уже давно и народ ноет что мол «не потоки».

              Для синхронного кода корутины без распределения по тредам профита не имеют, для асинхронного и для управления флоу async/await и промисы дают как по мне минимальную разницу в контексте языка построенного на event loop.

              Файберы это уже другое дело…


              1. lega
                28.10.2015 23:41

                я не вижу принципиальной разницы между использованием
                В данном случае разницы нет, разница видна в большом приложении, когда одно написано в синхронном стиле, а второе обвешано промисами и колбеками, например если в синхронном «дереве» вызова появится асинхронная ф-ия, то все дерево придется перепиливать на async/promise а так же все связанные части проекта, когда с корутинами этого ничего делать не нужно.

                Для синхронного кода корутины без распределения по тредам профита не имеют
                Не знаю что вы имеете ввиду, но в Python есть gevent и greenlet и результат будет как от файберов, т.е. код синхронный, но работает асинхронно.


                1. faiwer
                  29.10.2015 07:42

                  например если в синхронном «дереве» вызова появится асинхронная ф-ия, то все дерево придется перепиливать на async/promise а так же все связанные части проекта, когда с корутинами этого ничего делать не нужно.
                  Правильно ли я вас понимаю, что синхронное «дерево» остаётся синхронным, но необходимая часть выполняется асинхронно? Если да, то как это может работать не блокируя текущий поток? В JS он 1, всё таки.


                  1. lega
                    29.10.2015 07:56

                    Да, правильно.
                    На пальцах например так: когда вы вызываете «блокирующий» метод http.Get, он запускается асинхронно и при этом поток переключается на другие задачи, когда другие задачи так же переключатся в «ожидание» и для вашего метода будет готов результат, то поток переключится обратно на ваш метод http.Get, вернет результат и ваш код пойдет работать дальше, для вас это будет как обычный синхронный код.
                    В разных языках, библиотеках разные реализации но идея одна, посмотрите как работают файберы.

                    Вот почему vintage говорит что async/await, yields, promise, callbacks… — это «прошлый век».


                    1. faiwer
                      29.10.2015 08:14

                      В разных языках, библиотеках разные реализации но идея одна, посмотрите как работают файберы.
                      Года 3 назад я пытался использовать node-fibers. Работало оно крайне препогано. Часто падало с сегфолтами, существенные проблемы с отловом ошибок, практически невозможно было это дело как то дебажить. Может быть сейчас стало лучше.


                  1. vintage
                    29.10.2015 07:58

                    Очень просто, корутина — это легковесный поток. Когда корутине нужно заблокироваться — она не блокирует системный поток, а переключает его на другой стек вызовов, позволяя ему продолжать обрабатывать события. И как только придёт нужное событие — происходит переключение обратно на стек корутины. Таким образом у нас получается много легковесных потоков в рамках одного системного потока, которые не жалко блокировать.


                    1. faiwer
                      29.10.2015 08:12

                      Вы меня запутали :)

                      Таким образом у нас получается много легковесных потоков в рамках одного системного потока, которые не жалко блокировать.
                      1. А как тут дело обстоит с гонками, дедлоками и пр.? Ведь если это легковесный поток, то это именно поток…
                      2. Что-то мне подсказывает, что заморозить кадр в async-методе, и, когда нужно, разморозить его обратно ? должно быть гораздо дешевле, нежели запуск отдельного потока, пусть и легковесного. Так ли это?

                      Или под «легковесным потоком» вы подразумеваете не настоящий поток, а его имитацию, которая только приостанавливает выполнение одного стека, возобновляя другой, стоящий в очереди? Если да, то чем это отличается от await?


                      1. vintage
                        29.10.2015 08:45

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

                        2. Запуск легковесного потока требует лишь выделить память под стек и всё, переключение между стеками — тривиальная операция.

                        3. async создаёт «легковесный поток» для каждой функции.


                        1. lega
                          29.10.2015 08:59
                          +1

                          3. async создаёт «легковесный поток» для каждой функции.
                          Судя по тому что await — это «наследник» yield. То выходит что цикл обработчик создается в первой async ф-ии, и все дочерние async вызовы пробрасывают в него управление. Т.е. «легковесный поток» создается для «дерева» функций.


                        1. mayorovp
                          29.10.2015 09:04

                          Что выведет этот код?

                          var counter = 0;
                          var options = { delta: 1 }
                          
                          if (isDebug) trace(options);
                          increment(options)
                          increment(options)
                          increment(options)
                          console.log(counter);
                          
                          function increment(options) {
                              counter = counter + options.delta;
                          }
                          
                          function trace (object) {
                              for (var key in object)
                                (key => {
                                  var value = object[key];
                                  Object.defineProperty(key, {
                                    get: () => {
                                      sendEventToServer("get", object, key, value);
                                      return value;
                                    },
                                    set: newValue => {
                                      sendEventToServer("set", object, key, newValue);
                                      value = newValue;
                                    }, 
                                  });
                                })(key)
                          }
                          


                          1. mayorovp
                            29.10.2015 09:09

                            UPD: извиняюсь, совсем забыл. Три вызова increment надо запустить параллельно, а потом дождаться окончания их выполнения. В текущем виде будет 3 и только 3, параллельный вариант интереснее.


                      1. lega
                        29.10.2015 08:50

                        под «легковесным потоком» вы подразумеваете не настоящий поток, а его имитацию, которая только приостанавливает выполнение одного стека, возобновляя другой, стоящий в очереди?
                        Да.

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


                    1. mayorovp
                      29.10.2015 08:42
                      -1

                      Кажется, вы путаете сопрограммы с легковесными потоками. Отличаются они наличием планировщика потоков (заметьте, термина «планировщик корутин» не существует!)

                      Вот эта часть —

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


                      1. vintage
                        29.10.2015 08:51

                        Простейший планировщик потоков — event-loop — есть всегда.


                        1. mayorovp
                          29.10.2015 09:10
                          -1

                          Есть-то он есть, но сопрограммы не используют планировщик потоков. Даже простейший. Иначе они уже не сопрограммы, а легковесные потоки.


                          1. vintage
                            29.10.2015 18:38
                            -2

                            Человек не использует колёса для передвижения. Даже одно. Иначе это уже не человек, а инвалид.


            1. mayorovp
              29.10.2015 07:11

              Кажется, вы путаете «корутины» и «горутины». Вторые на Javascript невозможны в принципе — это однопоточный язык, и очень много кода на эту самую однопоточность завязано.


              1. lega
                29.10.2015 07:45
                -1

                Горутины — это корутины в Го, сотни корутин могут работать в одном потоке (для этого они и задумывались). А вот модуль для node.js — возможность использования корутин/файберов в javascript.



    1. Fesor
      28.10.2015 20:08

      Да как бы генераторы включены в стандарт ES2015, который уже принят, а с полифилами и babel.js работает и на ES5. Есть реализации, если вы почитаете комментарии в ветке, и даже не одна.

      async-await?

      async/await вы в этот список всетаки зря вписали так как по сути своей это синтаксический сахар для корутин при работе с асинхронными функциями. И для 90% юзкейсов это более чем удобный вариант.


      1. vintage
        28.10.2015 22:55
        +1

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


        1. lega
          28.10.2015 23:28

          Вообще, я потыкал Babel — в js async-await не так плох как в «синхронных» языках, т.к. в основе event-loop.
          — Из async ф-ии можно вызвать обычную, и эта обычная ф-ия может сработать как async (т.е. создать задержку) если вернуть Promise
          — Из обычной ф-ии можно вызвать async ф-ию и она запуститься асинхронно, но возможно есть/будет способ навесить callback
          — Исключения, вроде, работают как ожидается, правда traceback теряется.

          Ну и естественно ничего не заблочится, конечно это не корутины, но в полне себе не плохо.


    1. lega
      28.10.2015 22:33

      Есть патч для V8 который реализует файберы, но его наверно никогда не примут.