AngularJS - Promises заброшенные исключения
В следующем коде исключение захватывается функцией catch обещания $q:
// Fiddle - http://jsfiddle.net/EFpn8/6/
f1().then(function(data) {
console.log("success 1: "+data)
return f2();
})
.then(function(data) {console.log("success 2: "+data)})
.catch(function(data) {console.log("error: "+data)});
function f1() {
var deferred = $q.defer();
// An exception thrown here is not caught in catch
// throw "err";
deferred.resolve("done f1");
return deferred.promise;
}
function f2() {
var deferred = $q.defer();
// An exception thrown here is handled properly
throw "err";
deferred.resolve("done f2");
return deferred.promise;
}
Однако, когда я смотрю в вывод журнала консоли, я вижу следующее:
![enter image description here]()
Исключение было обнаружено в Angular, но также было уловлено обработкой ошибок браузера. Это поведение воспроизводится с библиотекой Q.
Это ошибка? Как я могу по-настоящему поймать исключение с помощью $q?
Ответы
Ответ 1
Исправлено с AngularJS версии 1.6
Причины такого поведения заключались в том, что непонятная ошибка отличается от обычного отклонения, поскольку
например, это может быть вызвано ошибкой программирования. На практике это оказалось путаным
или нежелательным для пользователей, поскольку ни родная promises, ни какая-либо другая популярная библиотека обещаний
отличает отброшенные ошибки от регулярных отклонений.
(Примечание. Хотя это поведение не противоречит спецификации Promises/A +, оно также не указывается.)
$д:
Из-за e13eea ошибка, передаваемая с помощью обработчиков обещаний onFulfilled
или onRejection
, обрабатывается точно так же, как и регулярная отказ. Ранее он также передавался $exceptionHandler()
(в дополнение к отказу от обещания с ошибкой в качестве причины).
Новое поведение относится ко всем службам/контроллерам/фильтрам и т.д., которые полагаются на $q
(включая встроенные службы, такие как $http
и $route
). Например, функции $http transformRequest/Response
или функция redirectTo маршрута, а также функции, указанные в объекте разрешения маршрута, больше не будут вызывать вызов $exceptionHandler()
, если они выдают ошибку. Кроме этого, все будет вести себя одинаково; то есть promises будет отклонен, переход маршрута будет отменен, события $routeChangeError
будут транслироваться и т.д.
- Руководство разработчика AngularJS - Перенос с V1.5 на V1.6 - $q
Ответ 2
Angular $q
использует соглашение, в котором заброшенные ошибки регистрируются независимо от того, что они были пойманы. Вместо этого, если вы хотите сигнализировать об отказе, вам нужно return $q.reject(...
как таковое:
function f2() {
var deferred = $q.defer();
// An exception thrown here is handled properly
return $q.reject(new Error("err"));//throw "err";
deferred.resolve("done f2");
return deferred.promise;
}
Это значит отличать отклонения от таких ошибок, как SyntaxError. Лично, это выбор дизайна, с которым я не согласен, но это понятно, так как $q
является крошечным, поэтому вы не можете построить надежный механизм необработанного отказа. В более сильных библиотеках, таких как Bluebird, такого рода вещи не требуются.
Как побочная заметка - никогда, никогда не бросайте строки: вы пропускаете трассировки стека таким образом.
Ответ 3
Это ошибка?
Нет. В источнике для $q показано, что преднамеренный блок try/catch создан для ответа на исключения, вызванные обратным вызовом, на
- Отклонение обещания, так как вы вызвали
deferred.reject
- Вызов обработчика исключений Angular. Как видно из $exceptionHandler docs, поведение по умолчанию этого заключается в том, чтобы зарегистрировать его на консоли браузера как ошибку, что и есть вы заметили.
... также был захвачен обработкой ошибок браузера
Чтобы пояснить, исключение не обрабатывается напрямую браузером, но появляется как ошибка, потому что Angular вызвал console.error
Как я могу действительно поймать исключение с помощью $q?
Обратные вызовы выполняются через некоторое время, когда текущий стек вызовов очищен, поэтому вы не сможете обернуть внешнюю функцию в блок try
/catch
. Однако у вас есть 2 варианта:
-
Вставьте try
/catch
блок вокруг кода, который может генерировать исключение, в обратном вызове:
f1().then(function(data) {
try {
return f2();
} catch(e) {
// Might want convert exception to rejected promise
return $q.reject(e);
}
})
-
Измените поведение службы Angular $exceptionHandler
, например, Как переопределить реализацию exception exceptionHandler. Вы могли бы изменить его, чтобы ничего не делать, поэтому в журнале ошибок консоли никогда не будет ничего, но я не думаю, что рекомендую это.
Ответ 4
Отложенное является устаревшим и действительно ужасным способом построения promises, использование конструктора решает эту проблему и многое другое:
// This function is guaranteed to fulfill the promise contract
// of never throwing a synchronous exception, using deferreds manually
// this is virtually impossible to get right
function f1() {
return new Promise(function(resolve, reject) {
// code
});
}
Я не знаю, поддерживает ли angular promises вышеупомянутое, если нет, вы можете сделать это:
function createPromise(fn) {
var d = $q.defer();
try {
fn(d.resolve.bind(d), d.reject.bind(d));
}
catch (e) {
d.reject(e);
}
return d.promise;
}
Использование такое же, как конструктор обещаний:
function f1() {
return createPromise(function(resolve, reject){
// code
});
}
Ответ 5
Вот пример теста, который показывает новую конструкторскую функцию $q, использование .finally(), отклонения и распространения цепочек обещаний:
iit('test',inject(function($q, $timeout){
var finallyCalled = false;
var failValue;
var promise1 = $q.when(true)
.then(function(){
return $q(function(resolve,reject){
// Reject promise1
reject("failed");
});
})
.finally(function(){
// Always called...
finallyCalled = true;
// This will be ignored
return $q.when('passed');
});
var promise2 = $q.when(promise1)
.catch(function(value){
// Catch reject of promise1
failValue = value;
// Continue propagation as resolved
return value+1;
// Or continue propagation as rejected
//return $q.reject(value+2);
});
var updateFailValue = function(val){ failValue = val; };
$q.when(promise2)
.then( updateFailValue )
.catch(updateFailValue );
$timeout.flush();
expect( finallyCalled ).toBe(true);
expect( failValue ).toBe('failed1');
}));