83

In my Angular.js application, I'm running some asynchronous operation. Before it starts I cover the application with a modal div, then once the operation is complete, I need to remove the div, whether the operation was successful or not.

Currently I have this:

LoadingOverlay.start(); 
Auth.initialize().then(function() {
    LoadingOverlay.stop();
}, function() {
    LoadingOverlay.stop(); // Code needs to be duplicated here
})

It works well, however I would prefer to have something cleaner like this pseudo-code:

LoadingOverlay.start(); 
Auth.initialize().finally(function() { // *pseudo-code* - some function that is always executed on both failure and success.
    LoadingOverlay.stop();
})

I assume it's quite a common problem, so I was thinking it could be done but cannot find anything in the doc. Any idea if it can be done?

laurent
  • 88,262
  • 77
  • 290
  • 428
  • If you can chain one `then()`, then you can surely chain another ... `.initialize().then(...).then(...)`. There's no "finally" as such; the final handler is the last one specified. – Beetroot-Beetroot Apr 16 '13 at 20:05
  • 2
    @Beetroot-Beetroot, that won't work because if `initialize()` fails, you still need to declare both a "success" function and a "fail" function and duplicate code in there. – laurent Apr 17 '13 at 02:46
  • Won't work, or just inelegant? – Beetroot-Beetroot Apr 17 '13 at 04:03
  • @Beetroot-Beetroot, I mean it really won't work. I have updated my example to clarify what I mean. As you can see in example 1, the code needs to be duplicated, which is not just inelegant but also harder to maintain. In example 2 (pseudo-code), the function will be executed no matter what, which avoids duplicate code. Here just chaining `then()` functions won't help since I'd still need to handle both failure and success (even though I don't care if it succeeded or not). Perhaps what I'm trying to do cannot currently be done though. – laurent Apr 17 '13 at 05:53
  • 1
    Laurent, what you want isn't currently available in Angular's lightweight $q service, which provides promises with just one method, `.then()` - see "The Promise API" [here](http://docs.angularjs.org/api/ng.$q). The only freedom is to have one `.then()` or to chain multiple `.then()`s. You are not the first to wish for a more extensive promise API - the feature you want is formally requested [here](https://github.com/angular/angular.js/issues/1828). – Beetroot-Beetroot Apr 17 '13 at 08:01
  • Feature request https://github.com/angular/angular.js/issues/2761 – OZ_ May 23 '13 at 11:48
  • @OZ_, I should have mentioned it but I actually ended up implementing the feature in this pull request - https://github.com/angular/angular.js/pull/2424 It's been accepted and I guess should be part of the main Angular.js release soon. – laurent May 23 '13 at 12:52
  • Thank you, @Laurent. It's implemented in 1.1.5 which is on google CDN today :) I think it will not be worse if $http will have same functional "out of the box" :) At this moment I'm trying to figure out and learn how can I wrap $http with $q to get flow, described in your question. – OZ_ May 23 '13 at 12:55
  • @OZ_, I didn't check but I would assume `$http` makes use of the built-in promise service, doesn't it? – laurent May 23 '13 at 14:25
  • @Laurent, I thought built-in promise and $q - different things. But if not.. Will check right now. – OZ_ May 23 '13 at 20:33
  • 1
    Apparently `always(callback)` is not implemented or rolled back in angular 1.2.6. We have to use `finally` now. I wonder why the reserved word `finally` is better than `always`. – Aleyna Dec 24 '13 at 17:22
  • @Aleyna, interesting, when I've implemented the feature, the consensus was to use `always` since that's what jQuery is using and because `finally` is a reserved JS keyword. Eventually, it seems they went for `finally` anyway with the caveat `Because 'finally' is a reserved word in JavaScript and reserved keywords are not supported as property names by ES3, you'll need to invoke the method like 'promise['finally'](callback)' to make your code IE8 compatible`. Why not, but it seems a bit more trouble than needed. – laurent Dec 24 '13 at 17:34
  • And actually Android Browser will also throw an error if using `finally` without quotes. – laurent Dec 24 '13 at 17:35
  • @Laurent, I have checked out the discussion on your pull request. I would second `always` as well. Perhaps maintainers wished to stick with Q's original api. – Aleyna Dec 24 '13 at 17:46

4 Answers4

164

The feature has been implemented in this pull request and is now part of AngularJS. It was initially called "always" and then later renamed to finally, so the code should be as follow:

LoadingOverlay.start(); 
Auth.initialize().then(function() {
    // Success handler
}, function() {
    // Error handler
}).finally(function() {
    // Always execute this on both error and success
});

Note that since finally is a reserved keyword, it might be necessary to make it a string so that it doesn't break on certain browsers (such as IE and Android Browser):

$http.get('/foo')['finally'](doSomething);
laurent
  • 88,262
  • 77
  • 290
  • 428
  • 10
    For anyone finding this from a web search, the link in the answer referred to the function `always` but it was changed to `finally` as you can see in this commit (or in the source): https://github.com/angular/angular.js/commit/f078762d48d0d5d9796dcdf2cb0241198677582c – Austin Thompson Jul 10 '14 at 16:16
  • @this.lau_ does the finally() have to be the last call in the chain, or can I chain more then()s off of it? – Brian Sep 11 '14 at 14:17
  • 4
    @Brian `finally` returns a promise just like the rest, so you can chain it. However (at least on some versions of Angular) the convenience overloads `success` and `error` are only added to the immediate return of `$http`, so if you _start_ with `finally` you'll lose those methods. – drzaus Nov 30 '15 at 19:25
  • @drzaus, for the record, `success` and `error` were deprecated. https://docs.angularjs.org/api/ng/service/$http – greenoldman Jul 15 '16 at 17:40
7

I'm using Umbraco version 7.3.5 back end with AngularJS version 1.1.5 and found this thread. When I implemented the approved answer I got the error:

xxx(...).then(...).finally is not a function

What did work however was always. If anyone else using an old version of AngularJS finds this thread and can't use finally use this code instead

LoadingOverlay.start(); 
Auth.initialize().then(function() {
    // Success handler
}, function() {
    // Error handler
}).always(function() {
    // Always execute this on both error and success
});
Ogglas
  • 62,132
  • 37
  • 328
  • 418
2

For those not using angularJS, and if you're ok with catching the error (not sure if .finally() would do that), you could use .catch().then() to avoid the duplicated code.

Promise.resolve()
  .catch(() => {})
  .then(() => console.log('finally'));

The catch() might end up being useful anyway for logging or other cleanup. https://jsfiddle.net/pointzerotwo/k4rb41a7/

PointZeroTwo
  • 2,142
  • 1
  • 17
  • 15
1

I would use ngView to render the content of the page and trigger the removal of you modal on the event $viewContentLoaded. See http://docs.angularjs.org/api/ng.directive:ngView for that event and http://docs.angularjs.org/api/ng.$rootScope.Scope for the $on event listener.

ocolot
  • 718
  • 6
  • 18