【问题标题】:AngularJS - Promises rethrow caught exceptionsAngularJS - 承诺重新抛出捕获的异常
【发布时间】:2014-06-13 01:06:52
【问题描述】:

在以下代码中,$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 捕获异常?

【问题讨论】:

    标签: javascript angularjs promise angular-promise


    【解决方案1】:

    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 这样更强大的库中,这种事情不是必需的。

    附带说明 - 永远不要抛出字符串:这样会错过堆栈跟踪。

    【讨论】:

    • 你是认真的吗?返回一个新的被拒绝的承诺后的代码永远不会被调用。而且你不需要 deferred ,因为你使用 $q.reject() 创建了一个新的承诺......
    • @Sebastian 显然代码不会在throwreturn 之后被调用,所有相关方都很清楚这一点。此答案中的代码是从问题中复制的 - OP 询问如何发出错误信号,这就是您的操作方式(请参阅此答案中的 return $q.reject 如何替换 OP 问题中完全相同的周围代码中的 throw "foo"。请考虑再看看 OP 的代码。
    • 我知道,但是想想那些不知道这个事实的人。如果我发现其中存在潜在错误,我宁愿不复制粘贴错误代码。你也建议不要扔字符串。这也是我认为 VitalyB 如果他编写了如此低俗的代码就知道的事情 ;-)。
    • @Sebastian 我希望人们不要盲目地复制/粘贴我在此处发布的代码,而是尝试阅读和理解它,然后自己应用我在其中讨论的原则。
    • 这个例子让我更加困惑。它包含多个返回和令人困惑的 cmets。 “这里抛出异常......”。什么例外?这段代码中唯一的“throw”语句被注释掉了。
    【解决方案2】:

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

    不知道angular promise是否支持以上,如果不支持,可以这样:

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

    【讨论】:

    • “已弃用延迟”。你有一个链接可以解释更多吗?
    • 确实如此。这是我第一次听说。 Angular $q 文档指定延迟...
    • 我的意思不是字面上被 angular 弃用 - 有什么我可以使用的同义词
    • 过时了?被 Promise 构造函数取代?
    【解决方案3】:

    这是一个错误吗?

    没有。查看source for $q 揭示了一个故意的try / catch 块被创建来响应回调中抛出的异常

    1. 拒绝承诺,就像你打电话给deferred.reject一样
    2. 调用已注册的 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 服务的行为方式,例如 How to override $exceptionHandler implementation 。您可以将其更改为完全不执行任何操作,因此控制台的错误日志中永远不会有任何内容,但我认为我不建议这样做。

    【讨论】:

      【解决方案4】:

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

      【讨论】:

        【解决方案5】:

        在 AngularJS 1.6 版中修复

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

        $q:

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

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

        -- AngularJS Developer Guide - Migrating from V1.5 to V1.6 - $q

        【讨论】:

        • 好消息!感谢您发布更新的答案。 Angular 现在会触发 unhandledrejectionrejectionhandled 事件吗?
        猜你喜欢
        • 2018-04-05
        • 2015-04-02
        • 1970-01-01
        • 2011-06-27
        • 2020-12-17
        • 2021-04-01
        • 2014-08-18
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多