Подтвердить что ты не робот

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?

4b9b3361

Ответ 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');

}));