Промисы в ES6: паттерны и анти-паттерны

https://medium.com/datafire-io/es6-promises-patterns-and-anti-patterns-bbb21a5d0918
  • Перевод
Несколько лет назад, когда я начал работать в 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-проектах?
Что вы используете в своих проектах?

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

RUVDS.com 527,81
RUVDS – хостинг VDS/VPS серверов
Поделиться публикацией
Комментарии 63
  • +2

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

    • –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)
          );
      }
      • +6

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


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

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


        p.then().then()

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

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

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

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

            Так ведь автор этого и не утверждает.

            • 0
              Я и не говорил, что утверждает, но откуда тогда появился пункт «Множественный вызов .then()»? То есть он раньше так думал, вот и вопрос: почему он так подумал? Очевидно, не читал туториалы, не то что спецификацию, и скреативил, хотя промисы вообще-то не подразумевают такого использования.
        • 0
          Не проще ли сразу воспользоваться вторым атрибутом then()?

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

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

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

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

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

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

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

          • +2
            добавьте опрос, кто использует промисы, кто коллбеки а кто async-await
            • +3
              Добавили
              • 0

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

                • 0

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

                • 0

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

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

                  Так и есть

                  • +1

                    async/await делают последовательное выполнение промисов сказочно простым

                  • 0
                    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 в данном случае не лучшее решение — этот ведь синхронный перебор, и обработка файлов будет поочередной, а не параллельной. Или я не прав?
                    • 0
                      Запросы будут сделаны параллельно, а на выходе вы получите новый промис к-ый разрезолвится после того как придут ответы от всех промисов и будет содержать массив их ответов
                      • 0
                        Ага, спасибо! Буду знать.
                    • 0
                      В принципе пролистав статью ничего нового не увидел, но новичкам будет очень полезно. Автору совутую добавить сюда в паттерны yield'ы async'и и generator's)
                      • 0

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

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

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

                          • 0

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


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


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

                            • 0

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

                              • +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

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


                                  const delay = time => new Promise(resolve => setTimeout(resolve, time));
                                • 0
                                  Про promisify написано в первом же комменте этого треда

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


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

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


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

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


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

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

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

                                    • 0

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


                                       q
                                      .then(() => this.loading = true)
                                      .then(() => doSmth())
                                      .then(() => this.loading = false)
                              • 0
                                Не в защиту bluebird, но состояние гонки можно легко получить и в асинхронной модели.
                                Многие забывают, что ответы могут приходить не в том же порядке, что и запросы
                            • 0
                              Добавьте вариант «события» в опрос.
                              • +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

                                • 0
                                  Достаточно вернуть промис.
                                  • 0
                                    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

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


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

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

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


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

                                          • 0

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

                                            • 0

                                              Смотрите, вот вариант с колбеком:


                                                  api.getItem(1, (err, item) => {
                                                      if (err) return cb(err);
                                              
                                                      try {
                                                          delete item.owner;
                                                          setTimeout(()=>{ item.owner.name; cb(); }, 0);
                                                      } catch (e) {
                                                          console.log("tt",e);
                                                          cb(e);
                                                      }
                                                  });

                                              Промизов нет, а ошибка — осталась.

                                              • +1

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

                                • 0
                                  Незнаю насколько это антипаттерн, но кое где использую такой велосипед (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);
                                    });
                                  }

                                  • +2
                                    Ваш код с Promise.all в ES6 можно переписать таким образом:
                                    function process(list) {
                                      return Promise.all([
                                        foo(),
                                        bar(),
                                        ...list.map(bar)
                                      ]);
                                    }
                                    


                                    И что за метод такой Promise.wait? В документации про него ни слова нету.
                                    • 0
                                      Не, ну это же упрощенный пример, перепешите тогда такой вариант (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
                                        Конечно можно начать конвертировать/рефакторить весь проект, превращать синхронные в ассинхронные/проброс промиса наверх

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


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

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

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

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

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

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

                                                Какие?

                                                • +1
                                                  python (и asyncio для асинхронщины), go, c/c++
                                                  мне этого хватает для большинства задач.
                                            • +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
                                              })

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

                                              • 0

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


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

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

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

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

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

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

                                        Либа достаточно неплохо помогала бороться со всем этим callback-hell'ом и с неё было проще пересесть на промисы =) Не знаю даже, используют ли её сейчас или нет…
                                        • 0
                                          Спасибо за статью. Для тех, кто совсем не сталкивался с промисами, рекомендую к прочтению темы Промисы на learn.javascript
                                          • –1
                                            С линейным выполнением промисов понятно(как показано в статье), а если необходимо ветвистая с обработкой ошибок на каждом этапе(просто выкинуть исключение круто, но чтобы продолжила программа работать дальше?). Как тут поступать?
                                            ЗЫ: В своих некоторых проектах, после каждого вызова есть обработчик ошибки, и логика парой не линейная, приходиться балансировать между «promisehill» и линейностью но код от этого не становиться читаем(С Async/await все более понятней).
                                            • НЛО прилетело и опубликовало эту надпись здесь
                                              • НЛО прилетело и опубликовало эту надпись здесь
                                                • +1

                                                  async/await уже 4 года используется в языке C#, и там пока не собираются отказываться от него. Почему вы думаете что в js аналогичная функциональность уже через 2 года будет признана не самой удачной?


                                                  PS на async/await не надо "радостно рефакторить". Надо сразу писать с его использованием.

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

                                                Самое читаемое