Доброго времени суток, друзья!

Представляю вашему вниманию перевод статьи Apal Shah «Common Javascript Promise mistakes every beginner should know and avoid».

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



Хотел бы я знать об этих ошибках, когда изучал 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)
})

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

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



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

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

Благодарю за внимание. Всем хороших выходных! Буду рад любым замечаниям.