Распространенные ошибки при работе с «промисами» в JavaScript, о которых должен знать каждый

Автор оригинала: Apal Shah
  • Перевод
Доброго времени суток, друзья!

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

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

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

Ошибка № 1. Использование блока try/catch внутри промиса


Использовать блок try/catch внутри промиса нецелесообразно, поскольку если Ваш код выдаст ошибку (внутри промиса), она будет перехвачена обработчиком ошибок самого промиса.

Речь идет вот о чем:
new Promise((resolve, reject) => {
  try {
    const data = someFunction()
    // ваш код
    resolve()
  } catch(e) {
    reject(e)
  }
})
  .then(data => console.log(data))
  .catch(error => console.log(error))

Вместо этого позвольте коду обработать ошибку вне промиса:
new Promise((resolve, reject) => {
  const data = someFunction()
  // ваш код
  resolve(data)
})
  .then(data => console.log(data))
  .catch(error => console.log(error))

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

Ошибка № 2. Использование асинхронной функции внутри промиса


При использовании асинхронной функции внутри промиса возникают некоторые неприятные побочные эффекты.

Допустим, Вы решили выполнить некоторую асинхронную задачу, добавили в промис ключевое слово «async», и Ваш код выдает ошибку. Однако теперь Вы не можете обработать эту ошибку ни с помощью .catch(), ни с помощью await:
// этот код не сможет перехватить ошибку
new Promise(async() => {
  throw new Error('message')
}).catch(e => console.log(e.message))

// этот код также не сможет перехватить ошибку
(async() => {
  try {
    await new Promise(async() => {
      throw new Error('message')
    })
  } catch(e) {
    console.log(e.message)
  }
})();

Каждый раз, когда я встречаю асинхронную функцию внутри промиса, я пытаюсь их разделить. И у меня это получается в 9 из 10 случаев. Тем не менее, это не всегда возможно. В таком случае у Вас нет другого выбора, кроме как использовать блок try/catch внутри промиса (да, это противоречит первой ошибке, но это единственный выход):
new Promise(async(resolve, reject) => {
  try {
    throw new Error('message')
  } catch(error) {
    reject(error)
  }
}).catch(e => console.log(e.message))

// или используя async/await
(async() => {
  try {
    await new Promise(async(resolve, reject) => {
      try {
        throw new Error('message')
      } catch(error) {
        reject(error)
      }
    })
  } catch(e) {
    console.log(e.message)
  }
})();

Ошибка № 3. Забывать про .catch()


Эта одна из тех ошибок, о существовании которой даже не подозреваешь, пока не начнется тестирование. Либо, если Вы какой-нибудь атеист, который не верит в тесты, Ваш код обязательно рухнет в продакшне. Потому что продакшн строго следует закону Мерфи, который гласит: «Anything that can go wrong will go wrong» (можно перевести так: «Если что-то может пойти не так, это обязательно произойдет»; аналогией в русском языке является «закон подлости» — прим. пер.).

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

Ошибка № 4. Не использовать Promise.all()


Promise.all() — твой друг.

Если Вы профессиональный разработчик, Вы наверняка понимаете, что я хочу сказать. Если у Вас есть несколько не зависящих друг от друга промисов, Вы можете выполнить их одновременно. По умолчанию, промисы выполняются параллельно, однако если Вам необходимо выполнить их последовательно (с помощью await), это займет много времени. Promise.all() позволяет сильно сократить время ожидания:
const {promisify} = require('util')
const sleep = promisify(setTimeout)

async function f1() {
  await sleep(1000)
}
async function f2() {
  await sleep(2000)
}
async function f3() {
  await sleep(3000)
}

// выполняем последовательно
(async() => {
  console.time('sequential')
  await f1()
  await f2()
  await f3()
  console.timeEnd('sequential') // около 6 секунд
})();

Теперь с Promise.all():
(async() => {
  console.time('concurrent')
  await Promise.all([f1(), f2(), f3()])
  console.timeEnd('concurrent') // около 3 секунд
})();

Ошибка № 5. Неправильное использование Promise.race()


Promise.race() не всегда делает Ваш код быстрее.

Это может показаться странным, но это действительно так. Я не утверждаю, что Promise.race() — бесполезный метод, но Вы должны четко понимать, зачем его используете.

Вы, например, можете использовать Promise.race() для запуска кода после разрешения любого из промисов. Но это не означает, что выполнение кода, следующего за промисами, начнется сразу же после разрешения одного из них. Promise.race() будет ждать разрешения всех промисов и только после этого освободит поток:
const {promisify} = require('util')
const sleep = promisify(setTimeout)

async function f1() {
  await sleep(1000)
}
async function f2() {
  await sleep(2000)
}
async function f3() {
  await sleep(3000)
}

(async() => {
  console.time('race')
  await Promise.race([f1(), f2(), f3()])
})();

process.on('exit', () => {
 console.timeEnd('race') // около 3 секунд, код не стал быстрее!
})

Ошибка № 6. Злоупотребление промисами


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

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

1) создать файл script.js следующего содержания (с лишними промисами):
new Promise((resolve) => {
  // некий код, возвращающий данные пользователя
  const user = {
    name: 'John Doe',
    age: 50,
  }
  resolve(user)
}).then(userObj => {
    const {age} = userObj
    return age
}).then(age => {
  if(age > 25) {
    return true
  }
throw new Error('Age is less than 25')
}).then(() => {
  console.log('Age is greater than 25')
}).catch(e => {
  console.log(e.message)
})

2) открыть командную строку (для пользователей Windows: чтобы открыть командную строку в папке с нужным файлом, зажимаем Shift, кликаем правой кнопкой мыши, выбираем «Открыть окно команд»), запустить script.js с помощью следующей команды (должен быть установлен Node.js):
node --trace-events-enabled script.js

3) Node.js создает файл журнала (в моем случае node_trace.1.txt) в папке со скриптом;

4) открываем Chrome (потому что это работает только в нем), вводим в адресной строке «chrome://tracing»;

5) нажимаем Load, загружаем файл журнала, созданного Node.js;

6) открываем вкладку Promise.

Видим примерно следующее:


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

Перепишем script.js:
new Promise((resolve, reject) => {
  const user = {
    name: 'John Doe',
    age: 50,
  }
  if(user.age > 25) {
    resolve()
  } else {
    reject('Age is less than 25')
  }
}).then(() => {
  console.log('Age is greater than 25')
}).catch(e => {
  console.log(e.message)
})

Повторим «трассировку».

Видим следующее:


Зеленых блоков (промисов) стало меньше, а значит время выполнения кода сократилось.

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

Благодарю за внимание.

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 12

    +4
    Promise.race() будет ждать завершения всех промисов
    Ну, как бы, это неправда. Promise.race() точно не будет ждать завершения всех промисов, он будет ждать только одного из них.

    А вот вся программа в целом будет ждать завершения всех (ну почти, были вроде исключения) промисов, созданных за время её выполнения. Что и показывает код в примере.
      0

      Либо автор оригинальной статьи уже исправил этот момент, либо тут очень вольный перевод. aio350 перепроверьте, пожалуйста :)

        +1
        спасибо, поправил
          0
          Но это не означает, что выполнение кода, следующего за промисами, начнется сразу же после разрешения одного из них

          But that doesn't mean that it will exit the code immediately after that

          Вы что-то не то поправили. Речь идёт о том, что само приложение не умрёт до тех пор пока есть хотя бы 1 событие, которого оно ждёт. Достаточно одного повисшего callback-а, чтобы приложение не умерло само. Но вот выполнение кода следующего за промисами начнётся как раз сразу после "разрешения одного из них"


          Там автор пишет:


          It will wait until all the promises get resolved and only after that, it will release the thread

          Не знаю зачем он решил всех запутать словом thread, которое имеет 100500 значений. Но по сути речь идёт о всём контексте в котором запущена js-VM. В случае nodejs это или весь процесс, или worker. В случае браузера это снова или worker или браузерный tab. А сам Promise.prototype.race работает именно так, как от него и ожидают.

            +1
            Речь идёт о том, что само приложение не умрёт до тех пор пока есть хотя бы 1 событие, которого оно ждёт.
            Тут скорее особенность не конкретно промисов, а всего рантайма целиком. В нём в принципе нет каких-то системных средств для досрочного прерывания работы функций, они будут только там, где вы сами их напишите. Вручную сбрасывать таймеры, вручную отменять http-запросы (что тоже работает не везде), делать «флаги смерти» и перепроверять их перед каждым действием внутри функции и т.д.
        +2
        new Promise((resolve, reject) => {
          const data = someFunction()
          // ваш код
          resolve()
        })
          .then(data => console.log(data))
          .catch(error => console.log(error))
        Чтобы ловить дату в then, её не помешало бы для начала передать в resolve.
        Использование асинхронной функции внутри промиса
        Тут скорее вопрос, а нахрена так изначально делать? Асинхронная функция при вызове так и так возвращает промис, зачем ёе скармливать в конструктор обычного промиса?
          0
          с data согласен, исправил. я тоже задавался этим вопросом, когда переводил статью
          0
          Ошибка № 4. Не использовать Promise.all()
          Если у Вас есть несколько не зависящих друг от друга промисов, Вы можете выполнить их одновременно.

          IMHO тут важно новичку обьяснить что значит «не зависящих друг от друга промисов». С потолка беру пример, но сталкивался похожей ситуацией.

          Ну вот пришел новичек, видит 2 функции (с):

          1. downloadFile(fileURL) — скачивает файлы, долго…
          2. findUserById(userId) — находит юзера в базе, сравнительно быстро
          3. someFoo(file, userId)

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

          Новичек запихивает это все в PromiseAll. И вроде все правильно, две функции не зависят друг от друга. Работает быстрее.

          Но, я встречал людей которые не понимают что если зафейлится findUserById(), функция downloadFile() не отменяется… и продолжает работать.

          В итоге, когда все хороше — да, быстрее. Но когда случаются ошибки… мемори лик, пустая трата ресурсов.

          Попадался код который ну просто везде юзает Promise.all. Понимает ли новичек что все это не бесплатно, и что в конце концов это все равно пойдет в очередь, и не важно на каком уровне? Нода, нетворк… в итоге вся система страдает, вместо того что бы иметь несколько пускай не очень быстрых мест.

          Поправьте если я не прав…
            +1
            console.timeEnd('race') // около 3 секунд, код не стал быстрее!


            у меня секунда

            image
              0

              Вы поставили timeEnd не в process.on('exit'), а куда надо. Вот оно у вас и работает как надо. В общем это трудности перевода :)

              +1
              Ошибка № 2. Использование асинхронной функции внутри промиса

              Не могу понять, это глупая ошибка новичка, или действительно бывают ситуации когда такое может понадобиться?

                0
                ситуация довольно странная. сначала масштабируют, а потом код оптимизируют…

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

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