18

在下面的代码中, $q promise的catch函数捕获了一个异常:

// 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;
}  

但是,当我查看控制台日志输出时,我看到以下内容:

在此处输入图像描述

该异常在 Angular 中被捕获,但也被浏览器的错误处理捕获。这种行为确实会在 Q 库中重现。

它是一个错误吗?我如何才能真正用 $q 捕获异常?

4

5 回答 5

16

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 这样更强大的库中,不需要这种东西。

作为旁注 - 永远不会抛出字符串:你会错过堆栈跟踪。

于 2014-04-27T15:24:16.223 回答
6

它是一个错误吗?

不。查看 $q的源代码表明,创建了一个故意的 try / catch 块来响应回调中抛出的异常

  1. 拒绝承诺,就像通过你的呼唤deferred.reject
  2. 调用已注册的 Angular 异常处理程序。从$exceptionHandler 文档中可以看出,默认行为是将其作为错误记录到浏览器控制台,这是您所观察到的。

...也被浏览器的错误处理捕获

澄清一下,异常不是由浏览器直接处理,而是显示为错误,因为 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服务的行为方式,例如How to override $exceptionHandler implementation。您可以将其更改为完全不执行任何操作,因此控制台的错误日志中永远不会有任何内容,但我认为我不会推荐这样做。

于 2014-04-27T16:00:24.590 回答
5

使用 AngularJS 1.6 版修复

这种行为的原因是未捕获的错误与常规拒绝不同,因为它可能是由例如编程错误引起的。在实践中,这对用户来说是令人困惑或不受欢迎的,因为本地承诺和任何其他流行的承诺库都没有将抛出的错误与常规拒绝区分开来。(注意:虽然这种行为不违反 Promises/A+ 规范,但也没有规定。)

$q:

由于e13eeaonFulfilled ,从 Promise或处理程序抛出的错误被onRejection视为与常规拒绝完全相同。以前,它也会被传递给$exceptionHandler()(除了以错误为理由拒绝承诺)。

新行为适用于所有依赖的服务/控制器/过滤器等$q(包括内置服务,例如$http$route)。例如,$http's transformRequest/Response函数或路由的 redirectTo 函数以及在路由的解析对象中指定的函数,$exceptionHandler()如果它们抛出错误,将不再导致调用。除此之外,一切都将继续以相同的方式运行;即承诺将被拒绝,路由转换将被取消,$routeChangeError事件将被广播等。

-- AngularJS 开发者指南 - 从 V1.5 迁移到 V1.6 - $q

于 2017-02-15T13:31:08.467 回答
4

deferred 是一种过时且非常糟糕的构造 Promise 的方式,使用构造函数解决了这个问题等等:

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

我不知道角度承诺是否支持上述内容,如果没有,您可以这样做:

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

用法与 promise 构造函数相同:

function f1() {
    return createPromise(function(resolve, reject){
        // code
    });
}
于 2014-04-27T15:25:17.913 回答
1

这是一个示例测试,展示了新的 $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');

}));
于 2015-07-19T03:41:49.113 回答