Ошибка на сервере promise

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

Время прочтения
5 мин

Просмотры 17K

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

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

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

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

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

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

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

Последнее обновление: 30.08.2021

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

const myPromise = new Promise(function(resolve, reject){
	console.log("Выполнение асинхронной операции");
	reject("Переданы некорректные данные");
});
myPromise.catch( function(error){
	console.log(error);
});

Функция catch() в качестве параметра принимает обработчик ошибки. Параметром этой функции-обработчика является то значение,
которое передается в reject().

Консольный вывод:

Выполнение асинхронной операции
Переданы некорректные данные

Генерация ошибки

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

const myPromise = new Promise(function(resolve){
	console.log("Выполнение асинхронной операции");
	getSomeWork();		// вызов не существующей функции
	resolve("Hello world!");
});
myPromise.catch( function(error){
	console.log(error);
});

Поскольку функция getSomeWork() нигде не объявлена, то выполнение асинхронной задачи завершится ошибкой и не дойдет до вызова resolve("Hello world!").
Поэтому сработает функция обработки ошибок из catch(), которая через параметр error получит информацию о возникшей ошибке, и
в консоли браузера мы увидим сообщение об ошибке:

Выполнение асинхронной операции
ReferenceError: getSomeWork is not defined
    at index.html:39
    at new Promise (<anonymous>)
    at index.html:37

throw

Также ошибка может быть результатом вызова оператора throw, который генерирует ошибку:

cconst myPromise = new Promise(function(resolve, reject){
	console.log("Выполнение асинхронной операции");
	const parsed = parseInt("Hello");
	if (isNaN(parsed)) { 
		throw "Not a number";			// Генерируем ошибку
	}
	resolve(parsed);
});
myPromise.catch( function(error){
	console.log(error);
});

Здесь парсится в число случайная строка. И если результат парсинга не представляет число, то с помощью оператора throw генерируем ошибку.
Это придет к завершению всей функции с ошибкой. И в итоге результат будет обработан функцией catch:

Выполнение асинхронной операции
Not a number

В этом случае функция обработчика получает сообщение об оошибке, который указывается после оператора throw.

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

if (isNaN(parsed)) { 
	reject("Not a number");
}

Однако данный оператор может применяться во внешней функции, которую мы вызываем в коде:

function getNumber(str){
	const parsed = parseInt(str);
	if (isNaN(parsed)) throw "Not a number";			// Генерируем ошибку
	else return parsed;
}
const myPromise = new Promise(function(resolve){
	console.log("Выполнение асинхронной операции");
	const result = getNumber("hello");
	resolve(result);
});
myPromise.catch( function(error){
	console.log(error);
});

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

try..catch

Как и в общем случае, операции, которые могут генерировать ошибку, можно помещать в конструкцию try..catch, а при возникновении исключения в блоке catch вызывать функцию reject():

const myPromise = new Promise(function(resolve, reject){
	try{
		console.log("Выполнение асинхронной операции");
		getSomeWork();		// вызов не существующей функции
		resolve("Hello world!");
	}
	catch(err){
		reject(`Произошла ошибка: ${err.message}`);
	}
});
myPromise.catch( function(error){
	console.log(error);
});

Консольный вывод:

Выполнение асинхронной операции
Произошла ошибка: getSomeWork is not defined

Обработка ошибки с помощью функции then

Кроме функции catch для получения информации об ошибке и ее обработки также можно использовать функцию
then() — ее второй параметр представляет обработчик ошибки, который в качестве параметра получает переданное из функции
reject значение:

promise
  .then(function(value){
    // получение значения
  },
  function(error){
    // обработка ошибки
  });
 

Второй параметр функции then() представляет функцию обработчика ошибок. С помощью параметра error в функции-обработчика мы можем получить переданное в reject() значение, либо информацию о возникшей ошибке.

Рассмотрим следуюший пример:

function generateNumber(str){ 
	return new Promise(function(resolve, reject){
		const parsed = parseInt(str);
		if (isNaN(parsed))	reject("значение не является числом")
		else resolve(parsed);
	})
	.then(function(value){ console.log("Результат операции:", value);}, 
		function(error){ console.log("Возникла ошибка:", error);});
}

generateNumber("23");
generateNumber("hello");

В данном случае для того, чтобы в промис можно было передать разные данные, он определен как возващаемый результат функции generateNumber(). То есть в данном случае консольный вывод будет следующим:

Результат операции: 23
Возникла ошибка: значение не является числом

Promise — это удобный способ для обработки асинхронных операций в JavaScript, который помог нам уйти от «callback hell» в нашем коде. Однако многие разработчики не до конца понимают, что происходит у промисов под капотом и используют их неправильно, теряя все преимущества этой прекрасной технологии.

Давайте разбираться, какие ошибки встречаются особенно часто и как их можно избежать.

Ошибка #1. Promise Hell

Убегая от «ада коллбэков», мы иногда попадаем в «ад промисов»:

userLogin('user').then(function(user){
    getArticle(user).then(function(articles){
        showArticle(articles).then(function(){
            // ...
        });
    });
});

В этом фрагменте сразу три промиса, вложенных друг в друга: userLogin, getArticle и showArticle. Сложность растет с каждой строчкой, и этот код уже довольно сложно читать. По сути мы обменяли шило на мыло, используя callback-style с промисами.

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

userLogin('user')
  .then(getArticle)
  .then(showArticle)
  .then(function(){
       // ...
});

Ошибка #2. try/catch внутри промиса

Блок try/catch нужен для обработки ошибок, но использовать его внутри промиса — плохая идея. В этом просто нет необходимости. Если внутри произойдет что-то нехорошее, Promise автоматически обработает это сам.

new Promise((resolve, reject) => {
  try {
    const data = doThis();
    // ...
    resolve();
  } catch (e) {
    reject(e);
  }
})
  .then(data => console.log(data))
  .catch(error => console.log(error));

Все ошибки (даже опечатки) внутри тела промиса будут замечены самим промисом и перенаправлены в обработчик из метода catch. В результате мы получим отклоненное обещание (rejected).

new Promise((resolve, reject) => {
  const data = doThis();
  // do something
  resolve()
})
  .then(data => console.log(data))
  .catch(error => console.log(error));

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

Ошибка #3. Асинхронные функции внутри промиса

Синтаксис Async/Await — это очень удобный способ работать с промисами в синхронном стиле.

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

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

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

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

// Ошибка не будет обработана

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);
  }
})();

Объединяя async-функцию с Promise, мы пытаемся вынести асинхронную логику из обещания, оставив его синхронным. Это работает, но не всегда.

Если же вам просто необходимо использовать именно асинхронную функцию, то обязательно добавляйте в нее 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);
  }
})();

Ошибка #4. Немедленное выполнение промиса после создания

const myPromise = new Promise(resolve => {
  // код самого HTTP-запроса
  resolve(result);
});

В этом фрагменте мы поместили HTTP-запрос внутрь промиса.

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

somePromise().then(myPromise);

Но это не так. Функция, переданная в конструктор Promise, выполняется сразу же. То есть ваш HTTP-запрос запускается сразу же после создания обещания. В этом легко убедиться:

const myPromise = new Promise(resolve => {
  console.log('start request');
  // код самого HTTP-запроса
  resolve(result);
});

В консоли сразу же появится сообщение о начале выполнения запроса, хотя вы не вызывали myPromise.

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

У самих промисов нет встроенного способа сделать это, однако вы всегда можете обратиться к нативным методам JavaScript. Если нет способа отложить выполнение коллбэка промиса, то просто отложите создание этого промиса.

const createMyPromise = () => new Promise(resolve => {
  // HTTP-запрос
  resolve(result);
});

Это «ленивый» промис.

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

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

Для этого существует метод Promise.all().

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

const sleep = delay => new Promise(res =>  setTimeout(res, delay))

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 секунд!

Выполнение последовательных вызовов заняло 6 секунд

Пример параллельного выполнения того же кода:

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

В два раза быстрее, так как нам потребовалось дождаться только выполнения самой долгой функции f3:

Выполнение параллельных вызовов заняло 3 секунды

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

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

  • « Предыдущая статья
  • Следующая статья »

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

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

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

function doSomethingOldStyle(successCallback, failureCallback) {
  console.log("Готово.");
  // Успех в половине случаев.
  if (Math.random() > .5) {
    successCallback("Успех")
  } else {
    failureCallback("Ошибка")
  }
}

function successCallback(result) {
  console.log("Успешно завершено с результатом " + result);
}

function failureCallback(error) {
  console.log("Завершено с ошибкой " + error);
}

doSomethingOldStyle(successCallback, failureCallback);

…современные функции возвращают промис, в который вы записываете ваши колбэки:

function doSomething() {
  return new Promise((resolve, reject) => {
    console.log("Готово.");
    // Успех в половине случаев.
    if (Math.random() > .5) {
      resolve("Успех")
    } else {
      reject("Ошибка")
    }
  })
}

const promise = doSomething();
promise.then(successCallback, failureCallback);

…или просто:

doSomething().then(successCallback, failureCallback);

Мы называем это асинхронным вызовом функции. У этого соглашения есть несколько преимуществ. Давайте рассмотрим их.

Гарантии

В отличие от старомодных переданных колбэков промис даёт некоторые гарантии:

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

Но наиболее непосредственная польза от промисов — цепочка вызовов (chaining).

Цепочка вызовов

Общая нужда — выполнять две или более асинхронных операции одна за другой, причём каждая следующая начинается при успешном завершении предыдущей и использует результат её выполнения. Мы реализуем это, создавая цепочку вызовов промисов (promise chain).

Вот в чём магия: функция then возвращает новый промис, отличающийся от первоначального:

let promise = doSomething();
let promise2 = promise.then(successCallback, failureCallback);

или

let promise2 = doSomething().then(successCallback, failureCallback);

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

По сути, каждый вызванный промис означает успешное завершение предыдущих шагов в цепочке.

Раньше выполнение нескольких асинхронных операций друг за другом приводило к классической «Вавилонской башне» колбэков:

doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Итоговый результат: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

В современных функциях мы записываем колбэки в возвращаемые промисы — формируем цепочку промисов:

doSomething().then(function(result) {
  return doSomethingElse(result);
})
.then(function(newResult) {
  return doThirdThing(newResult);
})
.then(function(finalResult) {
  console.log('Итоговый результат: ' + finalResult);
})
.catch(failureCallback);

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

doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
  console.log(`Итоговый результат: ${finalResult}`);
})
.catch(failureCallback);

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

Цепочка вызовов после catch

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

new Promise((resolve, reject) => {
    console.log('Начало');

    resolve();
})
.then(() => {
    throw new Error('Где-то произошла ошибка');

    console.log('Выведи это');
})
.catch(() => {
    console.log('Выведи то');
})
.then(() => {
    console.log('Выведи это, несмотря ни на что');
});

В результате выведется данный текст:

Начало
Выведи то
Выведи это, несмотря ни на что

Заметьте, что текст «Выведи это» не вывелся, потому что «Где-то произошла ошибка» привела к отказу

Распространение ошибки

Вы могли ранее заметить, что failureCallback повторяется три раза в «pyramid of doom», а в цепочке промисов всего лишь один раз:

doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Итоговый результат: ${finalResult}`))
.catch(failureCallback);

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

try {
  let result = syncDoSomething();
  let newResult = syncDoSomethingElse(result);
  let finalResult = syncDoThirdThing(newResult);
  console.log(`Итоговый результат: ${finalResult}`);
} catch(error) {
  failureCallback(error);
}

Эта симметрия с синхронным кодом лучше всего показывает себя в синтаксическом сахаре async/await в ECMAScript 2017:

async function foo() {
  try {
    let result = await doSomething();
    let newResult = await doSomethingElse(result);
    let finalResult = await doThirdThing(newResult);
    console.log(`Итоговый результат: ${finalResult}`);
  } catch(error) {
    failureCallback(error);
  }
}

Работа данного кода основана на промисах. Для примера здесь используется функция doSomething(), которая встречалась ранее. Вы можете прочитать больше о синтаксисе здесь

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

Создание промиса вокруг старого колбэка

Promise может быть создан с помощью конструктора. Это может понадобится только для старых API.

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

setTimeout(() => saySomething("10 seconds passed"), 10000);

Смешивание старого колбэк-стиля и промисов проблематично. В случае неудачного завершения saySomething или программной ошибки, нельзя обработать ошибку.

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

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);

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

Композиция

Promise.resolve() и Promise.reject() короткий способ создать уже успешные или отклонённые промисы соответственно. Это иногда бывает полезно.

Promise.all() и Promise.race() — два метода запустить асинхронные операции параллельно.

Последовательное выполнение композиции возможно при помощи хитрости JavaScript:

[func1, func2].reduce((p, f) => p.then(f), Promise.resolve());

Фактически, мы превращаем массив асинхронных функций в цепочку промисов равносильно: Promise.resolve().then(func1).then(func2);

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

const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));

composeAsync функция примет любое количество функций в качестве аргументов и вернёт новую функцию которая примет в параметрах начальное значение, переданное по цепочке. Это удобно, потому что некоторые или все функции могут быть либо асинхронными, либо синхронными, и они гарантированно выполнятся в правильной последовательности:

const transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2);
transformData(data);

В ECMAScript 2017, последовательные композиции могут быть выполнены более простым способом с помощью async/await:

for (const f of [func1, func2]) {
  await f();
}

Порядок выполнения

Чтобы избежать сюрпризов, функции, переданные в then никогда не будут вызваны синхронно, даже с уже разрешённым промисом:

Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2

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

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait().then(() => console.log(4));
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
console.log(1); // 1, 2, 3, 4

Вложенность

Простые цепочки promise лучше оставлять без вложений, так как вложенность может быть результатом небрежной структуры. Смотрите распространённые ошибки.

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

doSomethingCritical()
.then(result => doSomethingOptional()
  .then(optionalResult => doSomethingExtraNice(optionalResult))
  .catch(e => {})) // Игнорируется если необязательные параметр не выкинул исключение
.then(() => moreCriticalStuff())
.catch(e => console.log("Критическая ошибка: " + e.message));

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

Внутренний оператор catch нейтрализует и перехватывает ошибки только от doSomethingOptional() и doSomethingExtraNice(), после чего код возобновляется с помощью moreCriticalStuff(). Важно, что в случае сбоя doSomethingCritical() его ошибка перехватывается только последним (внешним) catch.

Частые ошибки

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

// Плохой пример! Три ошибки!

doSomething().then(function(result) {
  doSomethingElse(result) // Забыл вернуть промис из внутренней цепочки + неуместное влаживание
  .then(newResult => doThirdThing(newResult));
}).then(() => doFourthThing());
// Забыл закончить цепочку методом catch

Первая ошибка это неправильно сцепить вещи между собой. Такое происходит когда мы создаём промис но забываем вернуть его. Как следствие, цепочка сломана, но правильнее было бы сказать что теперь у нас есть две независимые цепочки, соревнующиеся за право разрешится первой. Это означает, что doFourthThing() не будет ждать doSomethingElse() или doThirdThing() пока тот закончится, и будет исполнятся параллельно с ними, это, вероятно, не то что хотел разработчик. Отдельные цепочки также имеют отдельную обработку ошибок, что приводит к необработанным ошибкам.

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

Третья ошибка это забыть закончить цепочку ключевым словом catch. Незаконченные цепочки приводят к необработанным отторжениям промисов в большинстве браузеров.

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

doSomething()
.then(function(result) {
  return doSomethingElse(result);
})
.then(newResult => doThirdThing(newResult))
.then(() => doFourthThing())
.catch(error => console.log(error));

Обратите внимание что () => x это сокращённая форма () => { return x; }.

Теперь у нас имеется единственная определённая цепочка с правильной обработкой ошибок.

Использование async/await предотвращает большинство, если не все вышеуказанные ошибки, но взамен появляется другая частая ошибка — забыть ключевое слово await.

Смотрите также

Понравилась статья? Поделить с друзьями:
  • Ошибка на сервере 504 дневник ру
  • Ошибка на сервере 502 школьный портал
  • Ошибка на сервере 500 что это такое
  • Ошибка на сервере 500 втб банк
  • Ошибка на сервере 1с предприятия произошла неисправимая ошибка приложение будет закрыто