Несколько лет назад, когда я начал работать в Node.js, меня приводило в ужас то, что сейчас известно как «ад коллбэков». Но тогда из этого ада выбраться было не так уж и просто. Однако, в наши дни Node.js включает в себя самые свежие, самые интересные возможности JavaScript. В частности, Node, начиная с 4-й версии, поддерживает промисы. Они позволяют уйти от сложных конструкций, состоящих из коллбэков.

image

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

Обратите внимание на то, что здесь я буду использовать стрелочные функции. Если вы с ними не знакомы, стоит сказать, что устроены они несложно, но в этом случае советую прочесть материал об их особенностях.

Паттерны


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

?Использование промисов


Если вы применяете стороннюю библиотеку, которая уже поддерживает промисы, пользоваться ими довольно просто. А именно, нужно обратить внимание на две функции: then() и catch(). Например у нас имеется API с тремя методами: getItem(), updateItem(), и deleteItem(), каждый из которых возвращает промис:

Promise.resolve()
  .then(_ => {
    return api.getItem(1)
  })
  .then(item => {
    item.amount++
    return api.updateItem(1, item);
  })
  .then(update => {
    return api.deleteItem(1);
  })
  .catch(e => {
    console.log('error while working on item 1');
  })

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

Вот, для сравнения, реализация той же логики с помощью коллбэков:

api.getItem(1, (err, data) => {
  if (err) throw err;
  item.amount++;
  api.updateItem(1, item, (err, update) => {
    if (err) throw err;
    api.deleteItem(1, (err) => {
      if (err) throw err;
    })
  })
})

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

?Преобразование коллбэков в промисы


Один из первых приёмов, который полезно изучить при переходе с коллбэков на промисы, заключается в преобразовании коллбэков в промисы. Потребность в подобном может возникнуть в том случае, если вы, например, работаете с библиотекой, которая всё ещё использует коллбэки, или с собственным кодом, написанном с их применением. Перейти от коллбэков к промисам не так уж и сложно. Вот пример преобразования функции Node fs.readFile, основанной на коллбэках, в функцию, которая задействует промисы:

function readFilePromise(filename) {
  return new Promise((resolve, reject) => {
    fs.readFile(filename, 'utf8', (err, data) => {
      if (err) reject(err);
      else resolve(data);
    })
  })
}

readFilePromise('index.html')
  .then(data => console.log(data))
  .catch(e => console.log(e))

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

Обратите внимание на то, что в результате должно быть вызвано что-то одно — либо resolve, либо reject, и этот вызов должен быть выполнен лишь один раз. В нашем примере, если fs.readFile возвращает ошибку, мы передаём эту ошибку в reject. В противном случае мы передаём данные файла в resolve.

?Преобразование значений в промисы


В ES6 есть пара удобных вспомогательных функций для создания промисов из обычных значений. Это Promise.resolve() и Promise.reject(). Например, у вас может быть функция, которой нужно возвратить промис, но которая обрабатывает некоторые случаи синхронно:

function readFilePromise(filename) {
  if (!filename) {
    return Promise.reject(new Error("Filename not specified"));
  }
  if (filename === 'index.html') {
    return Promise.resolve('<h1>Hello!</h1>');
  }
  return new Promise((resolve, reject) => {/*...*/})
}

Обратите внимание на то, что вы можете передать что угодно (или ничего) при вызове Promise.reject(), однако, рекомендуется всегда передавать этому методу объект Error.

?Одновременное выполнение промисов


Promise.all() — это удобный метод для одновременного выполнения массива промисов. Например, скажем, у нас есть список файлов, которые мы хотим прочитать с диска. С использованием созданной ранее функции readFilePromise, решение этой задачи может выглядеть так:

let filenames = ['index.html', 'blog.html', 'terms.html'];

Promise.all(filenames.map(readFilePromise))
  .then(files => {
    console.log('index:', files[0]);
    console.log('blog:', files[1]);
    console.log('terms:', files[2]);
  })

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

?Последовательное выполнение промисов


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

Одно из решений этой проблемы заключается в том, чтобы запускать промисы последовательно, один за другим. К сожалению, в ES6 нет простого аналога Promise.all для выполнения подобной операции (хотелось бы знать — почему?), но тут нам может помочь метод Array.reduce:

let itemIDs = [1, 2, 3, 4, 5];

itemIDs.reduce((promise, itemID) => {
  return promise.then(_ => api.deleteItem(itemID));
}, Promise.resolve());

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

Promise.resolve()
  .then(_ => api.deleteItem(1))
  .then(_ => api.deleteItem(2))
  .then(_ => api.deleteItem(3))
  .then(_ => api.deleteItem(4))
  .then(_ => api.deleteItem(5));

?Гонка промисов


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

Например, создадим промис, который завершается с ошибкой по прошествии некоторого времени, задавая ограничение на выполнение операции по чтению файла, представленной другим промисом:

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(reject, ms);
  })
}

Promise.race([readFilePromise('index.html'), timeout(1000)])
  .then(data => console.log(data))
  .catch(e => console.log("Timed out after 1 second"))

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

?Перехват ошибок


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

Promise.resolve()
  .then(_ => api.getItem(1))
  .then(item => {
    item.amount++;
    return api.updateItem(1, item);
  })
  .catch(e => {
    console.log('failed to get or update item');
  })

Здесь вызывается блок catch(), если либо getItem, либо updateItem завершится с ошибкой. Но что, если совместная обработка ошибок нам не нужна и требуется обрабатывать ошибки, происходящие в getItem, раздельно? Для этого достаточно вставить ещё один блок catch() сразу после блока с вызовом getItem — он даже может вернуть другой промис:

Promise.resolve()
  .then(_ => api.getItem(1))
  .catch(e => api.createItem(1, {amount: 0}))
  .then(item => {
    item.amount++;
    return api.updateItem(1, item);
  })
  .catch(e => {
    console.log('failed to update item');
  })

Теперь, если getItem() даст сбой, мы вмешиваемся и создаём новый элемент.

?Выбрасывание ошибок


Код внутри выражения then() стоит воспринимать так, будто он находится внутри блока try. И вызов return Promise.reject(), и вызов throw new Error() приведут к выполнению следующего блока catch().

Это означает, что ошибки времени выполнения также вызывают срабатывание блоков catch(), поэтому, когда дело доходит до обработки ошибок, не стоит делать предположений об их источнике. Например, в следующем фрагменте кода мы можем ожидать, что блок catch() будет вызван только для обработки ошибок, появившихся при работе getItem, но, как показывает пример, он реагирует и на ошибки времени выполнения, возникшие внутри выражения then():

api.getItem(1)
  .then(item => {
    delete item.owner;
    console.log(item.owner.name);
  })
  .catch(e => {
    console.log(e); // Cannot read property 'name' of undefined
  })

?Динамические цепочки промисов


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

function readFileAndMaybeLock(filename, createLockFile) {
  let promise = Promise.resolve();

  if (createLockFile) {
    promise = promise.then(_ => writeFilePromise(filename + '.lock', ''))
  }

  return promise.then(_ => readFilePromise(filename));
}

В подобной ситуации нужно обновить значение promise, использовав конструкцию вида promise = promise.then(/*...*/). С этим примером связано то, что мы рассмотрим ниже в разделе «Множественный вызов .then()».

Анти-паттерны


Промисы — это аккуратная абстракция, но работа с ними полна подводных камней. Тут мы рассмотрим некоторые типичные проблемы, с которыми мне доводилось сталкиваться, работая с промисами.

?Реконструкция ада коллбэков


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

api.getItem(1)
  .then(item => {
    item.amount++;
    api.updateItem(1, item)
      .then(update => {
        api.deleteItem(1)
          .then(deletion => {
            console.log('done!');
          })
      })
  })

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

?Отсутствие команды возврата


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

api.getItem(1)
  .then(item => {
    item.amount++;
    api.updateItem(1, item);
  })
  .then(update => {
    return api.deleteItem(1);
  })
  .then(deletion => {
    console.log('done!');
  })

Ошибка заключается в том, что мы не поместили вызов return перед api.updateItem в строке 4, и этот конкретный блок then() разрешается немедленно. В результате api.deleteItem(), вероятно, будет вызвано до завершения вызова api.updateItem().

По моему мнению, это — основная проблема с промисами ES6, и она часто ведёт к их непредсказуемому поведению. Проблема заключается в том, что then() может вернуть либо значение, либо новый объект Promise, при этом он вполне может вернуть и undefined. Лично я, если бы отвечал за API промисов JavaScript, предусмотрел бы выдачу ошибки времени выполнения, если бы блок .then() возвращал undefined. Однако, подобное в языке не реализовано, поэтому сейчас нам лишь остаётся быть внимательными и выполнять явный возврат из любого создаваемого нами промиса.

?Множественный вызов .then()


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

let p = Promise.resolve('a');
p.then(_ => 'b');
p.then(result => {
  console.log(result) // 'a'
})

let q = Promise.resolve('a');
q = q.then(_ => 'b');
q = q.then(result => {
  console.log(result) // 'b'
})

В этом примере, так как мы не обновляем значение p при следующем вызове then(), мы никогда не увидим возврата 'b'. Промис q более предсказуем, его мы обновляем каждый раз, вызывая then().

То же самое применимо и к обработке ошибок:

let p = Promise.resolve();
p.then(_ => {throw new Error("whoops!")})
p.then(_ => {
  console.log('hello!'); // 'hello!'
})

let q = Promise.resolve();
q = q.then(_ => {throw new Error("whoops!")})
q = q.then(_ => {
  console.log('hello'); // Сюда мы никогда не попадём
})

Тут мы ожидаем выдачу ошибки, которая прервёт выполнение цепочки промисов, но так как значение p не обновляется, мы попадаем во второй then().

Множественный вызов .then() позволяет создать из исходного промиса несколько новых независимых промисов, однако, мне до сих пор не удалось найти реального применения для этого эффекта.

?Смешивание коллбэков и промисов


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

function getThing(callback) {
  api.getItem(1)
    .then(item => callback(null, item))
    .catch(e => callback(e));
}

getThing(function(err, thing) {
  if (err) throw err;
  console.log(thing);
})

Проблема здесь заключается в том, что в случае ошибки мы получим предупреждение «Unhandled promise rejection», несмотря на то, что блок catch() в цепочке присутствует. Это так из-за того, что callback() вызывается и внутри then(), и внутри catch(), что делает его частью цепочки промисов.

Если вам абсолютно необходимо обернуть промис в коллбэк, вы можете использовать функцию setTimeout, или process.nextTick в Node.js для того, чтобы выйти из промиса:

function getThing(callback) {
  api.getItem(1)
    .then(item => setTimeout(_ => callback(null, item)))
    .catch(e => setTimeout(_ => callback(e)));
}

getThing(function(err, thing) {
  if (err) throw err;
  console.log(thing);
})

?Неперехваченные ошибки


Обработка ошибок в JavaScript — странная штука. Она поддерживает классическую парадигму try/catch, но не поддерживает средства обработки ошибок в вызванном коде вызывающей его конструкцией, как это сделано, например, в Java. Однако, в JS распространено использование коллбэков, первым параметром которых является объект ошибки (такой коллбэк называют ещё «errback»). Это вынуждает конструкцию, вызывающую метод, как минимум, учитывать возможность ошибки. Вот пример с библиотекой fs:

fs.readFile('index.html', 'utf8', (err, data) => {
  if (err) throw err;
  console.log(data);
})

Работая с промисами, легко забыть о том, что ошибки надо явным образом обрабатывать. Особенно это актуально в тех случаях, когда речь идёт об операциях, восприимчивым к ошибкам, таким, как команды для работы с файловой системой или для доступа к базам данных. В текущих условиях, если не перехватить отклонённый промис, в Node.js можно увидеть довольно-таки неприглядное предупреждение:

(node:29916) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: whoops!
(node:29916) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

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

Итоги


Мы рассмотрели некоторые паттерны и анти-паттерны использования промисов. Надеюсь, вы нашли здесь что-нибудь полезное. Однако, тема промисов весьма обширна, поэтому вот — несколько ссылок на дополнительные ресурсы:


Уважаемые читатели! Как вы используете промисы в своих Node.js-проектах?

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


  1. MrCheater
    05.10.2017 15:15
    +2

    Хочу дополнить тему "Преобразование коллбэков в промисы"
    Так как написано в статье, делать нынче уже не принято. Есть util.promisify


  1. Aingis
    05.10.2017 15:37
    -3

    Про util.promisify меня уже опередили, но, похоже, вы не до конца разобрались в теме промисов.

    let p = Promise.resolve('a');
    p.then(_ => 'b');
    p.then(result => {
      console.log(result) // 'a'
    })
    Только что проверил в консоли:
    Promise.resolve('a')
        .then(_ => 'b')
        .then(result => {
            console.log(result); // 'b'
        });
    

    Всё дело в том, когда резолвится промис. Если сделать Promise.resolve() от не thennable значения, то он будет разрезолвен сразу.

    Если вам абсолютно необходимо обернуть промис в коллбэк, вы можете использовать функцию setTimeout, или process.nextTick в Node.js для того, чтобы выйти из промиса:
    function getThing(callback) {
      api.getItem(1)
        .then(item => setTimeout(_ => callback(null, item)))
        .catch(e => setTimeout(_ => callback(e)));
    }
    Не проще ли сразу воспользоваться вторым атрибутом then()? Тогда проблемы двойного вызова не будет.

    function getThing(callback) {
        api.getItem(1).then(
            item => callback(null, item),
            e => callback(e)
        );
    }


    1. justboris
      05.10.2017 16:01
      +6

      Первые два фрагмента кода отличаются


      p.then(...);
      p.then(...);

      это не то же самое что


      p.then().then()

      Поэтому в первом случае a, а во втором b, никаких подвохов.


      1. Aingis
        06.10.2017 14:16
        -2

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

        p.then(...);
        p.then(...);
        колбэки выполняются последовательно. Но с чего автор взял, что в следующий колбэк не из цепочки придёт результат предыдущего? Это ему никто не обещал, и это ниоткуда не следует. Тот случай, когда сам придумал — сам опроверг.

        P.S. Путаницы ещё и добавляет фраза в тексте
        По моему мнению, это — основная проблема с промисами ES6, и она часто ведёт к их непредсказуемому поведению. Проблема заключается в том, что then() может вернуть либо значение, либо новый объект Promise, при этом он вполне может вернуть и undefined.
        Это не так! then() всегда возвращает Promise. А вот колбэк может вернуть как Promise, так и любое значение. В результате, даже когда знаешь правильный ответ, сложно понять что имелось в виду на самом деле.

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


    1. justboris
      05.10.2017 16:04

      Не проще ли сразу воспользоваться вторым атрибутом then()?

      При подходе promise.then(() => doSomething()).catch(...) блок catch перехватит не только ошибки в оригинальном промисе, но и если что-то пойдет не так в doSomething. Это бывает полезно.


      1. Aingis
        05.10.2017 16:39

        Спасибо, кэп! Но пункт был не в этом.


        1. justboris
          05.10.2017 16:40

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


  1. saaivs
    05.10.2017 15:55
    +3

    Пробежал по диагонали. Первое, что пришло в голову — это поском по страницы поискать «async/await». На момент написания комментария нашел единственное упоминание в разделе ПОХОЖИЕ ПУБЛИКАЦИИ под названием «Async/await: 6 причин забыть о промисах»

    Собственно, что и хотел сказать :)

    PS: Понимаю, что перевод и «мопед не мой», но все-таки…


    1. wert_lex
      05.10.2017 20:25
      +2

      Ну, для того, чтобы полноценно пользоваться async/await понимать как работают промисы всё же необходимо.


  1. ilnuribat
    05.10.2017 16:03
    +2

    добавьте опрос, кто использует промисы, кто коллбеки а кто async-await


    1. ru_vds Автор
      05.10.2017 16:20
      +3

      Добавили


      1. vintage
        05.10.2017 18:37

        Опять про node-fibers забыли :-(


        1. kahi4
          05.10.2017 18:40

          И стримы (rx, bacon, kefir)


      1. dale0
        05.10.2017 18:42

        А как это оценивать? В одном проекте может понадобиться использовать все три подхода.


  1. vtvz_ru
    05.10.2017 18:17
    +2

    Спасибо за указанные тонкости и приемы работы с промисами. Теперь мой код станет чуточку лучше)
    Предпочитаю использовать async/await. Но насколько я знаю, async/await всего лишь синтаксисический сахар над промисами, и ничто не мешает мне использовать Promise.all с функциями, которые async. Или я ошибаюсь?


    1. RubaXa
      05.10.2017 18:37
      +1

      Так и есть


  1. maolo
    06.10.2017 08:26

    let filenames = ['index.html', 'blog.html', 'terms.html'];
    
    Promise.all(filenames.map(readFilePromise))
      .then(files => {
        console.log('index:', files[0]);
        console.log('blog:', files[1]);
        console.log('terms:', files[2]);
      })


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


    1. wheercool
      06.10.2017 09:28

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


      1. maolo
        06.10.2017 10:34

        Ага, спасибо! Буду знать.


  1. Senyaak
    06.10.2017 08:26

    В принципе пролистав статью ничего нового не увидел, но новичкам будет очень полезно. Автору совутую добавить сюда в паттерны yield'ы async'и и generator's)


  1. jehy
    06.10.2017 09:57

    Забыли про генераторы и про то, что даже с нативной поддержкой промисы, bluebird быстрее и обладает чудесным богатым api. Пользуюсь им в разработке на восьмой ноде.


    1. wheercool
      06.10.2017 10:02
      +1

      В новой версии V8 уже не актуально


      1. jehy
        06.10.2017 12:09

        Если мне не изменяет память, то нативные были в 5-7 раз медленее bluebird. Ускорение нативных в два раза будет маловато (кроме того, подозреваю, что bluebird при этом тоже ускорился). Свежих бенчмарков ни у кого нет?


    1. mayorovp
      06.10.2017 12:22

      Из bluebird выкинуты многие полезные доработки, которые вошли в нативные.


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


      Так что начинающим (а пост-то явно для них написан) я бы bluebird не рекомендовал.


      1. jehy
        06.10.2017 12:30

        delay, cancel, spread, promisify, timeout, inspection — там есть огромное количество фич, без которых, конечно, жить можно, но гораздо печальнее. В нативных, кажется, даже finally нет.


        1. justboris
          06.10.2017 12:52
          +1

          • Про promisify написано в первом же комменте этого треда
          • delay = promisify(setTimeout)
          • spread — деструктуризация аргументов дает то же самое Promise.all(...).then(([a,b,c]) => console.log(a,b,c))
          • cancel — успешно решается на уровне библиотек. Например в axios это делается через токен.
          • finally есть в обычном try/catch, который можно использовать в асинхронных функциях.

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


          1. mayorovp
            06.10.2017 13:12
            +1

            Нее, delay = promisify(setTimeout) работать не будет, порядок аргументов не тот. Тут надо вручную:


            const delay = time => new Promise(resolve => setTimeout(resolve, time));


            1. justboris
              06.10.2017 15:15

              А вы попробуйте.


              Нодовский promisify позволяет подменять возвращаемый результат через специальный символ. И для setTimeout этот кастомный символ уже определен


          1. jehy
            06.10.2017 13:46

            Про promisify написано в первом же комменте этого треда

            Да я видел, но мне не нравится мне использовать его из util. Из Promise гораздо логичнее и чище.


            cancel — успешно решается на уровне библиотек

            Ага, например, на уровне request-promise это решается через cancel от bluebird. Зачем плодить лишние сущности?


            finally есть в обычном try/catch, который можно использовать в асинхронных функциях.

            Ээээ. Так мне нужно его в цепочке использовать, finally от обычного try catch вот ни разу не поможет. Или вы имели в виду — использовать его в связке с async/await?


            C delay уже сказали, для timeout тоже надо будет какой-то костыль делать… Кстати, со стэком у меня как-то никогда не было проблем, а про race condition фразу я, честно говоря, не понял.


            1. mayorovp
              06.10.2017 13:53

              q.then(() => this.loading = false);
              this.loading = true;

              Если продолжение будет выполнено синхронно — получится упс.


              1. jehy
                06.10.2017 14:18

                Как-то не возникало мысли так писать, гораздо логичнее


                 q
                .then(() => this.loading = true)
                .then(() => doSmth())
                .then(() => this.loading = false)


      1. wheercool
        06.10.2017 12:35

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


  1. koeshiro
    06.10.2017 11:55

    Добавьте вариант «события» в опрос.


  1. freeart
    06.10.2017 12:53
    +1

    Вообще-то в catch войдет только синхронный вызов throw


    api.getItem(1)
      .then(item => {
            delete item.owner;
            //async throw
            setTimeout(()=>{item.owner.name},0)
      })
      .catch(e => {
        console.log("tt",e); // Cannot read property 'name' of undefined
      })

    встречал это непонимание концепции в различных библиотеках при ловле ошибок и ожидание попадания обработчика в catch


    1. Aingis
      06.10.2017 13:09

      Достаточно вернуть промис.


      1. freeart
        06.10.2017 13:17

        api.getItem(1)
          .then(item => {
                delete item.owner;
                //async throw
                return new Promise((resolve)=>{
                        setTimeout(()=>{item.owner.name; resolve()},0)
                })
          })
          .catch(e => {
            console.log("tt",e); // Cannot read property 'name' of undefined
          })

        Так? setTimeout это пример реализации сторонней, либо своей библиотеки в которой произошла ошибка при асинхронном вызове. Т.е. метод библиотеки вернул промис, но внутри он упал на ошибке.


        1. mayorovp
          06.10.2017 13:43
          -1

          Конечно же если чужая библиотека намеренно или случайно прячет от вас ошибку — ее достать не получится.


          Но это просто означает что библиотека кривая.


          1. freeart
            06.10.2017 13:48

            а вы видели где-то библиотеку или продукт лишенный ошибок? Мир не совершенен. И люди, которые ждут от промис катч слишком многого.


  1. lega
    06.10.2017 14:33

    Незнаю насколько это антипаттерн, но кое где использую такой велосипед (foo, bar возвращают новые промисы):

    function process(list) {
      let promises = [];
      promises.push(foo());
      promises.push(bar());
      list.forEach(() => promises.push(bar()));
      return Promise.all(promises);
    }
    

    Вместо того чтобы вручную собирать все новые промисы, вызываю Promise.wait которая это делает за меня, код выше превращается в:
    function process(list) {
      return Promise.wait(() => {
        foo();
        bar();
        list.forEach(bar);
      });
    }


    1. hopefaq
      06.10.2017 15:02
      +2

      Ваш код с Promise.all в ES6 можно переписать таким образом:

      function process(list) {
        return Promise.all([
          foo(),
          bar(),
          ...list.map(bar)
        ]);
      }
      


      И что за метод такой Promise.wait? В документации про него ни слова нету.


      1. lega
        06.10.2017 17:02

        Не, ну это же упрощенный пример, перепешите тогда такой вариант (preload возвращает промис):

        class User {
            constructor(raw) {
                Object.assign(this, raw);
                this.links.forEach(preload);
                this.children = this.children.map((raw) => new User(raw));
            }
        }
        
        // sync variant
        var users = rawUsers.map((raw) => new User(raw));
        
        // async variant
        Promise.wait(() => {
            return rawUsers.map((raw) => new User(raw));
        }).then((users) => {});
        
        Суть в том что есть некий синхронный код с глубоким стеком и в определенный момент где-то на глубоком уровне появилась асинхронная операция, и вам сверху нужно дождаться её завершения. Конечно можно начать конвертировать/рефакторить весь проект, превращать синхронные в ассинхронные/проброс промиса наверх, но выглядит это не очень. Простой отлов новых промисов выглядит куда приятнее (и по большей части работает как надо).

        И что за метод такой Promise.wait?
        Я написал, что это велосипед.


        1. justboris
          06.10.2017 18:08
          +1

          Конечно можно начать конвертировать/рефакторить весь проект, превращать синхронные в ассинхронные/проброс промиса наверх

          Нужно начать рефакторить весь проект. В противном случае отхватите багов, потому что кто-то забудет поставить Promise.wait. Или наоборот, поставит Promise.wait внутри другого Promise.wait.


          Ну и покажите реализацию в коде, интересно посмотреть, какими хаками вы это сделали.


          1. lega
            06.10.2017 21:23

            Нужно начать рефакторить весь проект.
            И можете не вписаться в дедлайны…
            потому что кто-то забудет поставить Promise.wait
            А если кто-то забудет поставить Promise.all? Не пройдет тестирование и пофиксится. А вот если наченете рефакторить (а если там 500к кода?) то наделать ошибок шансов больше.
            Или наоборот, поставит Promise.wait внутри другого Promise.wait.
            Работает как и ожидается.

            какими хаками вы это сделали.
            Ничего сверх-естественного, например можно так в 15 строк (на проде вариант получше использую): jsfiddle.net/lega911/pvovavLe

            PS: может вы зарефакторите мой пример выше? интересно посмотреть как сильно распухнет код.


            1. vintage
              06.10.2017 22:11

              Зачем так извращаться, если есть node-fibers, который делает ровно то, что вам надо?


              1. lega
                06.10.2017 22:37

                node-fibers хорош, но он не работает в браузерах. А для сервер сайда я использую другие инструменты.


                1. vintage
                  07.10.2017 09:25

                  Какие?


                  1. lega
                    07.10.2017 13:05
                    +1

                    python (и asyncio для асинхронщины), go, c/c++
                    мне этого хватает для большинства задач.


            1. justboris
              06.10.2017 23:45
              +1

              Сайд-эффекты в конструкторе это уже плохо. У меня бы получилось как-то так


              class User {
                 constructor(raw) {
                   Object.assign(this, raw);
                   this.children = this.children.map(c => new User(c));
                 }
                 load() {
                   return fetch(...).then(() => Promise.all(
                      this.children.map(c => c.load())
                   ))
                 }
              }
              
              const users = rawUsers.map(user => new User(user));
              
              Promise.all(users.map(user => user.load())).then(() => {
               // что-то делаем с готовыми users
              })

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


            1. justboris
              06.10.2017 23:47

              Ну и по поводу вашей имплементации: а с нативными API, типа fetch оно как работает?


              Как я понимаю, они ваш переопредленный класс Promise не увидят, будут пользоваться стандартным.


              1. lega
                07.10.2017 01:57

                Сайд-эффекты в конструкторе
                Смотря что считать сайд-эффектами, вообщем это не аргумент.

                У меня бы получилось как-то так
                Код сложнее (имхо), кода больше, лишняя функция («логика/апи»), итого большой реальный код может не слабо распухнуть.

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

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

                Я не предлагаю отказываться от промисов, это просто ещё один подход.


  1. unel
    06.10.2017 15:02

    Вообще между callback hell и промисами был ещё один промежуточный этап в виде использования либы async (или даже логичней дать ссылку на более старую версию)

    Либа достаточно неплохо помогала бороться со всем этим callback-hell'ом и с неё было проще пересесть на промисы =) Не знаю даже, используют ли её сейчас или нет…