$q promise error callback chains - angularjs

In the following code snippet error 1 and success 2 will be logged. How can I can I propagate error callbacks being invoked rather than the success callbacks being invoked if the original deferred is rejected.
angular.module("Foo", []);
angular
.module("Foo")
.controller("Bar", function ($q) {
var deferred = $q.defer();
deferred.reject();
deferred.promise
.then(
/*success*/function () { console.log("success 1"); },
/*error*/function () { console.log("error 1"); })
.then(
/*success*/function () { console.log("success 2"); },
/*error*/function () { console.log("error 2"); });
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="Foo">
<div ng-controller="Bar"></div>
</div>

Error is propagate by returning $q.reject in the error callback
var deferred = $q.defer();
deferred.reject();
deferred.promise
.then(
/*success*/function () { console.log("success 1"); },
/*error*/function () { console.log("error 1"); return $q.reject('error 1')})
.then(
/*success*/function () { console.log("success 2"); },
/*error*/function () { console.log("error 2"); });
});

think of success/failure as try/catch
try{
var val = dummyPromise();
} catch (e){
val = "SomeValue";
}
if catch does not throws an exception, it is considered that the error is handled and hence outer calling function does not sees the error which occured in inner function.
Similar stuff happening here, you have to return return $q.reject(); from a promise in order for the next promise in the chain to fail too. See example plunker: http://plnkr.co/edit/porOG8qVg2GkeddzVHu3?p=preview
The reason is: Your error handler may take action to correct the error. In your error-function your dealing with the error,if not specified otherwise, it will return a new promise which is resolved. Therefore it is not reasonable to have the next promise failing by default (try-catch analogy).
By the way, you can return $q.reject() even from a success handler, if you sense an error condition, to have the next promise in the chain failing.
You're catching the error and handling it - so it gets to the success handler. If you want to reject it, you have to do it by returning $q.reject();

To sum the comments up, to propagate errors in the promise chain, either:
1) Do not provide an errorCallback for then:
deferred.promise
.then(
/*success*/function () { console.log("success 1"); },
.then(
/*success*/function () { console.log("success 2"); },
/*error*/function () { console.log("error 2"); }); // gets called
Or
2) Return $q.reject() from the errorCallback:
deferred.promise
.then(
/*success*/function () { console.log("success 1"); },
/*error*/function (err) { console.log("error 1"); return $q.reject(err); });
.then(
/*success*/function () { console.log("success 2"); },
/*error*/function () { console.log("error 2"); }); // gets called
From the angular $q.reject documentation:
This api should be used to forward rejection in a chain of promises.

Related

The deferred antipattern, how to handle rejects?

I understand what is The deferred antipattern, also mentioned as "The Forgotten Promise" HERE.
I also read: what-is-the-explicit-promise-construction-antipattern-and-how-do-i-avoid-it.
Anyways I should try to get rid of using $q.defer().
But I don't understand how to handle case where I got response and I need to reject it because it doesn't have proper key.
This is my method in Service that returns promise to controller.
self.findTime = function(data){
var deferred = $q.defer();
apiClient.findTime(data)
.then(function (response) {
if (response.result !== "success") {
deferred.reject(response);
}
deferred.resolve(response);
}
, function (error) {
deferred.reject(error);
console.error(error);
});
return deferred.promise;
};
So I tried to change it to:
self.findTime = function(data){
return apiClient.findTime(data)
.then(function (response) {
if (response.result !== "success") {
// return WHAT ??? <-------------
}
return response;
}
, function (error) {
// return WHAT ??? <-------------
console.error(error);
});
};
How to handle reject?
In controller, on reject I show some morning message.
Service.findTime(data).then(function (response) {
// do something
}, function (error) {
// show warn dialog
});
You have two options.
You can return a rejection promise from within your resolve:
return apiClient.findTime(data)
.then(function (response) {
if (response.result !== "success") {
return $q.reject('reason'); // Convert to rejection
}
return response;
}, function (error) {
console.error(error);
return $q.reject('reason'); // Chain rejection
});
or you can throw an exception:
return apiClient.findTime(data)
.then(function (response) {
if (response.result !== "success") {
throw new Error('reason');
}
return response;
}, function (error) {
console.error(error);
throw new Error(error);
});

Angular $q catch block resolves promis?

The past view days I read a lot of best practices in handling with promises. One central point of the most postings where something like this:
So if you are writing that word [deferred] in your code
[...], you are doing something wrong.1
During experimenting with the error handling I saw an for me unexpected behavior. When I chain the promises and It run into the first catch block the second promise gets resolved and not rejected.
Questions
Is this a normal behavior in other libs / standards (e.g. q, es6), too and a caught error counts as solved like in try / catch?
How to reject the promise in the catch block so that the second gets, called with the same error / response object?
Example
In this example you see 'I am here but It was an error'
Full Plunker
function BaseService($http, $q) {
this.$http = $http;
this.$q = $q;
}
BaseService.prototype.doRequest = function doRequest() {
return this.$http({
method: 'GET',
url: 'not/exisint/url'
})
.then(function (response) {
// do some basic stuff
})
.catch(function(response) {
// do some baisc stuff e.g. hide spinner
});
}
function ChildService($http, $q) {
this.$http = $http;
this.$q = $q;
}
ChildService.prototype = Object.create(BaseService.prototype);
ChildService.prototype.specialRequest = function specialRequest() {
return this.doRequest()
.then(function (response) {
alert('I am here but It was an error');
})
.catch(function (response) {
// do some more specific stuff here and
// provide e.g. error message
alert('I am here but It was an error');
return response;
});
}
Workaround:
With this workaround you can solve this problem, but you have to create a new defer.
BaseService.prototype.doRequest = function doRequest() {
var dfd = this.$q.defer();
return this.$http({
method: 'GET',
url: 'not/exisint/url'
})
.then(function (response) {
// do some basic stuff
dfd.resolve(response);
})
.catch(function(response) {
// do some basic stuff e.g. hide spinner
dfd.reject(error);
});
}
Your workaround is almost correct, you can simplify it to the following:
BaseService.prototype.doRequest = function doRequest() {
return this.$http({
method: 'GET',
url: 'not/exisint/url'
})
.then(function (response) {
// do some basic stuff
return response;
}, function (error) {
return this.$q.reject(error);
});
}
$q.reject is a shortcut to create a deferred that immediately get's rejected.
Yes, this is default behaviour in other libraries as well. .then or .catch simply wraps the return value into a new promise. You can return a rejected promise to make the .catch chain work.
You can also do the opposite, for instance when you want to reject the promise in the success callback for whatever reason:
function getData() {
return this.$http.get(endpoint).then(result => {
// when result is invalid for whatever reason
if (result === invalid) {
return this.$q.reject(result);
}
return result;
}, err => this.$q.reject(err));
}
getData().then(result => {
// skipped
}, error => {
// called
});
See example above
Just to add to Dieterg's answer and to your workaround, you can also wrap the code into $q constructor:
BaseService.prototype.doRequest = function doRequest() {
return $q(function (resolve, reject) {
$http.get('not/exisint/url').then(function (response) { // success
/* do stuff */
resolve(response);
}, function (error) { // failure
/* do stuff */
reject(error);
});
});
};

How to stop a promise chain when using $state.go() in angular?

I have a chain of promises that are responsible for initializing my controller. In this chain if a certain condition isn't met, it would be best to send the user to another state via $state.go() and stop the rest of the promise chain from running. How can this be accomplished?
loadData1()
.then(function(){
return loadData2();
})
.then(function(){
if (...) {
$state.go(...); // how should the existing promise chain be killed off or stopped?
}
else {
return loadData3();
}
})
.then(function(){
return loadData4();
})
.then(function(){
console.log('controller initialized successfully');
},
function(error){
console.log('failed to initialize controller');
});
Instead of immediately calling $state.go, throw an error and check for it in the error handler at the end.
loadData1()
.then(function () {
return loadData2();
})
.then(function () {
if (exceptionalCondition) {
throw new Error('[MyCtrl:loadData2] Data failed to load!');
}
return loadData3();
})
...
.then(function () {
console.log('controller initialized successfully');
},
function (error) {
if (/^\[MyCtrl:loadData2\]/.test(error.message)) {
$state.go(redirect);
} else {
console.log('failed to initialize controller');
}
});
The nice thing about using promises is that they will handle errors and immediately terminate the chain if one occurs.

how to throw an error in a $http promise

I have an angular service that wraps my rest api calls and returns a $http promise.
My question is how do I throw an error so that a promise that triggers the .error method gets called? I don't want to just throw error since I want it to use the .success/.error in the calling function rather than doing a try catch block around it.
myFunction: function(foo)
if (foo) {
return $http.put(rootUrl + '/bar', {foo: foo});
}
else {
//what do I return here to trigger the .error promise in the calling function
}
You don't need $q.defer(). And else too. You can use reject directly:
myFunction: function(foo) {
if (foo) {
return $http.put(rootUrl + '/bar', {foo: foo});
}
return $q.reject("reject reason");
}
See https://docs.angularjs.org/api/ng/service/$q#reject
You'll want to create your own promise using $q. Here's how I did something similar in a recent project:
app.service('allData', ['$http','$q',function($http,$q) {
return {
getJson: function() {
return $q(function(resolve, reject) { // return a promise
$http.get('/path/to/data.json', {cache:true})
.success(function(data) {
if (angular.isArray(data)) { // should be an ordered array
// or any other test you like that proves it's valid
resolve(data);
} else {
reject("Invalid JSON returned");
console.log(data);
};
})
.error(function(data) {
reject("Invalid data returned");
console.log(data);
});
});
}
};
}]);
And in my controller:
allData.getJson().then(function(json) {
// success, do something with the json
}, function(reason) { // failure, .getJson() had some kind of error
alert('Sorry, unable to retrieve data from the server.')
console.error(reason);
});
First inject the $q-service in your service. Then in your else:
else {
var deferred = $q.defer();
deferred.reject("reject reason, foo was false");
return deferred.promise;
}
Not as clever as Blazemonger's, but its quick to do.
You can raise or throw a custom error using throw new Error ("custom error").
For http:
http.get('url').toPromise().then (result =>{
throw new Error ("My Custom Error") // New Custom error New is optional w
}).catch(err => {
throw err
}); // catch will catch any error occur while http call

How do I test an $httpBackend POST error state with AngularJS / Karma?

I'm trying to write a unit test for my Angular Service and here's a function in the service:
login = function(authObject) {
deferred = $q.defer();
$http({
url: '/api/v1/session/create',
method: 'POST',
data: authObject
}).success(function(response) {
var user;
if (response.status === 'ok' && response.user && response.authenticated === true) {
user = response.user;
}
return deferred.resolve(response);
}).error(function(data) {
deferred.reject(data);
return $state.go('api_error');
});
return deferred.promise;
};
I can successfully test the success case with something like:
it('should go to the api error state', function() {
var authObject;
authObject = {
username: 'a#b.com',
password: 'c'
};
$httpBackend.expectPOST('/api/v1/session/create').respond(someData);
userService.login(authObject).then(function(response) {
return console.log("not error", response);
}, function(response) {
return console.log("error", response);
});
return expect($state.go).toHaveBeenCalledWith('api_error');
});
That works fine, however if I do:
$httpBackend.expectPOST('/api/v1/session/create').respond(500, 'error');, then the error case doesn't get called. What am I doing wrong?
In order for your .then() error callback to be called, the previous promise in the chain should result in error (e.g. throw an Exception) or be rejected.
Returning 500 will cause the error callback in your login() method to be called, but since that callback neither throws an Error nor gets rejected, your chained error callback won't be called.
E.g. changing:
}).error(function(data) {
deferred.reject(data);
return $state.go('api_error');
});
to:
}).error(function(data) {
return deferred.reject(data);
//return $state.go('api_error');
});
would work (but it doesn't do what you want :D).
I am not familiar with ui-router, but in this case it could be possible that $state.go() aborts the current execution chain, so I am not sure the following would work:
}).error(function(data) {
$state.go('api_error');
return deferred.reject(data);
});

Resources