JavaScript Промисы - reject против throw
Я прочитал несколько статей на эту тему, но все еще не совсем понимаю, в чем заключается разница между использованием Promise.reject
и выбрасыванием ошибки. Например,
Использование Promise.reject
return asyncIsPermitted()
.then(function(result) {
if (result === true) {
return true;
}
else {
return Promise.reject(new PermissionDenied());
}
});
Использование throw
return asyncIsPermitted()
.then(function(result) {
if (result === true) {
return true;
}
else {
throw new PermissionDenied();
}
});
Я предпочитаю использовать throw
, так как это короче, но меня интересует, есть ли какие-либо преимущества одного подхода над другим.
5 ответ(ов)
Нет никаких преимуществ в использовании одного подхода по сравнению с другим, однако есть конкретный случай, когда throw
не сработает. Тем не менее, эти случаи можно исправить.
В любой момент, когда вы находитесь внутри обратного вызова промиса, вы можете использовать throw
. Тем не менее, если вы находитесь в любом другом асинхронном обратном вызове, вы должны использовать reject
.
Например, следующий код не вызовет обработчик catch
:
new Promise(function() {
setTimeout(function() {
throw 'или нет';
// return Promise.reject('или нет'); тоже не сработает
}, 1000);
}).catch(function(e) {
console.log(e); // не произойдет
});
Вместо этого вы получите неопределенный промис и необработанное исключение. Это случай, когда вы предпочтете использовать reject
. Однако вы можете исправить это двумя способами.
- Используя функцию
reject
оригинального промиса внутри таймаута:
new Promise(function(resolve, reject) {
setTimeout(function() {
reject('или нет');
}, 1000);
}).catch(function(e) {
console.log(e); // работает!
});
- Промиссифицировав таймаут:
function timeout(duration) { // Спасибо joews
return new Promise(function(resolve) {
setTimeout(resolve, duration);
});
}
timeout(1000).then(function() {
throw 'работает!';
// return Promise.reject('работает'); тоже сработает
}).catch(function(e) {
console.log(e); // 'работает!'
});
Важный момент заключается в том, что reject()
НЕ завершает поток управления, как это делает оператор return
. В отличие от этого, throw
действительно завершает поток управления.
Вот пример:
new Promise((resolve, reject) => {
throw "err";
console.log("Никогда не достигнуто");
})
.then(() => console.log("Решено"))
.catch(() => console.log("Отклонено"));
в отличие от этого:
new Promise((resolve, reject) => {
reject(); // аналогично работает и resolve()
console.log("Всегда достигнуто"); // "Отклонено" будет выведено ПОСЛЕ этого
})
.then(() => console.log("Решено"))
.catch(() => console.log("Отклонено"));
В первом случае, при использовании throw
, управление сразу переходит в блок catch
, и строка "Никогда не достигнуто" не будет выполнена. Во втором случае, после вызова reject()
, код продолжит выполняться, и "Всегда достигнуто" будет выведено в консоль перед тем, как сработает catch
.
Да, самая большая разница заключается в том, что reject — это функция обратного вызова, которая выполняется после того, как промисс был отклонён, тогда как throw не может быть использован асинхронно. Если вы решите использовать reject, ваш код будет продолжать выполняться в асинхронном режиме, тогда как throw будет приоритизировать завершение функции разрешения (эта функция выполнится немедленно).
Пример, который помог мне понять эту тему, заключается в том, что вы можете установить функцию Timeout с reject, например:
new Promise((resolve, reject) => {
setTimeout(() => { reject('err msg'); console.log('finished'); }, 1000);
return resolve('ret val');
})
.then((o) => console.log("RESOLVED", o))
.catch((o) => console.log("REJECTED", o));
Такой код было бы невозможно написать с использованием throw:
try {
new Promise((resolve, reject) => {
setTimeout(() => { throw new Error('err msg'); }, 1000);
return resolve('ret val');
})
.then((o) => console.log("RESOLVED", o))
.catch((o) => console.log("REJECTED", o));
} catch (o) {
console.log("IGNORED", o);
}
В маленьком примере автора вопроса разница может быть незаметна, но при работе с более сложными асинхронными концепциями разница между двумя подходами может быть весьма значительной.
Кратко: Функция трудно использовать, когда она иногда возвращает промис, а иногда выбрасывает исключение. При написании асинхронной функции лучше сигнализировать о неудаче, возвращая отклонённый промис.
Ваш конкретный пример затушёвывает некоторые важные различия между этими подходами.
Поскольку вы обрабатываете ошибки внутри цепочки промисов, выброшенные исключения автоматически превращаются в отклонённые промисы. Это может объяснить, почему они кажутся взаимозаменяемыми, но это не так.
Рассмотрим следующую ситуацию:
checkCredentials = () => {
let idToken = localStorage.getItem('some token');
if (idToken) {
return fetch(`https://someValidateEndpoint`, {
headers: {
Authorization: `Bearer ${idToken}`
}
});
} else {
throw new Error('No Token Found In Local Storage');
}
}
Это будет антипаттерном, поскольку вам придётся поддерживать как асинхронные, так и синхронные случаи ошибок. Это может выглядеть примерно так:
try {
function onFulfilled() { /* ... остальная логика */ }
function onRejected() { /* обработка асинхронной ошибки - например, тайм-аут сети */ }
checkCredentials(x).then(onFulfilled, onRejected);
} catch (e) {
// Error('No Token Found In Local Storage')
// обработка синхронной ошибки
}
Это не совсем правильно, и именно здесь Promise.reject
(доступный в глобальной области видимости) приходит на помощь и тем самым эффективно отличается от throw
. Рефакторинг теперь выглядит так:
checkCredentials = () => {
let idToken = localStorage.getItem('some_token');
if (!idToken) {
return Promise.reject('No Token Found In Local Storage');
}
return fetch(`https://someValidateEndpoint`, {
headers: {
Authorization: `Bearer ${idToken}`
}
});
}
Теперь вы можете использовать только один catch()
для сетевых сбоев и для синхронной проверки отсутствия токена:
checkCredentials()
.catch((error) => {
if (error == 'No Token') {
// показать модальное окно без токена
} else if (error === 400) {
// показать модальное окно "не авторизован" и т.д.
}
});
Вот пример, который вы можете попробовать. Просто измените isVersionThrow
на false
, чтобы вместо выброса исключения использовать отклонение промиса.
const isVersionThrow = true
class TestClass {
async testFunction() {
if (isVersionThrow) {
console.log('Версия с выбрасыванием')
throw new Error('Неудача!')
} else {
console.log('Версия с отклонением')
return new Promise((resolve, reject) => {
reject(new Error('Неудача!'))
})
}
}
}
const test = async () => {
const test = new TestClass()
try {
var response = await test.testFunction()
return response
} catch (error) {
console.log('ОШИБКА ВОЗВРАЩЕНА')
throw error
}
}
test()
.then(result => {
console.log('результат: ' + result)
})
.catch(error => {
console.log('ошибка: ' + error)
})
Попробуйте изменить значение isVersionThrow
и посмотрите, как это повлияет на обработку ошибок. Если isVersionThrow
равно true
, будет выброшено исключение, а если false
— промис будет отклонён.
Использование async/await с циклом forEach
Синтаксис асинхронной стрелочной функции
Как преобразовать существующий API с обратными вызовами в промисы?
В чем разница между String.slice и String.substring?
Проверка соответствия строки регулярному выражению в JS