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
Related
I'm finding it hard to understand the "deferred antipattern". I think I understand it in principal but I haven't seen a super simple example of what a service, with a differed promise and one with antipattern, so I figured I'd try and make my own but seeing as how I'm not super in the know about it I'd get some clarification first.
I have the below in a factory (SomeFactory):
//url = 'data.json';
return {
getData: function(){
var deferred = $q.defer();
$http.get(destinationFactory.url)
.then(function (response) {
if (typeof response.data === 'object') {
deferred.resolve(response.data);
} else {
return deferred.reject(response.data);
}
})
.catch(function (error) {
deferred.reject(error);
});
return deferred.promise;
}
The reason I am checking its an object is just to add a simple layer of validation onto the $http.get()
And below, in my directive:
this.var = SomeFactory.getData()
.then(function(response) {
//some variable = response;
})
.catch(function(response) {
//Do error handling here
});
Now to my uderstanding, this is an antipattern. Because the original deferred promise catches the error and simply swallows it. It doesn't return the error so when this "getData" method is called I have do another catch to grab the error.
If this is NOT an antipattern, then can someone explain why both require a "callback" of sorts? When I first started writing this factory/directive I anticipated having to do a deffered promise somewhere, but I didn't anticipate having to .catch() on both sides (aka I was sort of thinking I could get the factory to return the response or the error if I did a SomeFactory.getData()
Is this a “Deferred Antipattern”?
Yes, it is. 'Deferred anti-pattern' happens when a new redundant deferred object is created to be resolved from inside a promise chain. In your case you are using $q to return a promise for something that implicitly returns a promise. You already have a Promise object($http service itself returns a promise), so you just need to return it!
Here's the super simple example of what a service, with a deferred promise and one with antipattern look like,
This is anti-pattern
app.factory("SomeFactory",['$http','$q']){
return {
getData: function(){
var deferred = $q.defer();
$http.get(destinationFactory.url)
.then(function (response) {
deferred.resolve(response.data);
})
.catch(function (error) {
deferred.reject(error);
});
return deferred.promise;
}
}
}])
This is what you should do
app.factory("SomeFactory",['$http']){
return {
getData: function(){
//$http itself returns a promise
return $http.get(destinationFactory.url);
}
}
while both of them are consumed in the same way.
this.var = SomeFactory.getData()
.then(function(response) {
//some variable = response;
},function(response) {
//Do error handling here
});
There's nothing wrong with either examples(atleast syntactically)..but first one is redundant..and not needed!
Hope it helps :)
I would say that it is the classic deferred anti-pattern because you are creating needless deferred objects. However, you are adding some value to the chain (with your validation). Typically, IMO, the anti-pattern is particularly bad when deferred objects are created for very little or no benefit.
So, the code could be much simpler.
$q promises have a little documented feature of automatically wrapping anything returned inside a promise in a promise (using $q.when). In most cases this means that you shouldn't have to manually create a deferred:
var deferred = $q.defer();
However, that is how the documentation demonstrates how to use promises with $q.
So, you can change your code to this:
return {
getData: function(){
return $http.get(destinationFactory.url)
.then(function (response) {
if (typeof response.data === 'object') {
return response.data;
} else {
throw new Error('Error message here');
}
});
// no need to catch and just re-throw
});
}
Using the $q constructor is a deferred anti-pattern
ANTI-PATTERN
vm.download = function() {
var url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf";
return $q(function(resolve, reject) {
var req = {
method: 'POST',
url: url,
responseType: 'arraybuffer'
};
$http(req).then(function(response) {
resolve(response.data);
}, function(error) {
reject(error);
});
});
}
CORRECT
vm.download = function() {
var url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf";
var req = {
method: 'POST',
url: url,
responseType: 'arraybuffer'
};
return $http(req).then(function(response) {
return response.data;
});
}
The $http service already returns a promise. Using the $q constructor is unnecessary and error prone.
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);
});
});
};
I have three methods:
myHub.server.getColumnSettings().done(function (result) {
if (result) {
//Do stuff with result
}
});
myHub.server.getDefaultGroupedBy().done(function(result) {
if (result) {
//Do stuff with result
}
});
function init() {
//Do more stuff
}
I would like getColumnsSettings to finish, and after that I want getDefaultGroupedBy to finish, and after that init().
I tried following, but it didn't work..
var defer = $q.defer();
defer.promise
.then(function() {
myHub.server.getColumnSettings().done(function (result) {
if (result) {
//Do stuff with result
}
});
})
.then(function() {
myHub.server.getDefaultGroupedBy().done(function(result) {
if (result) {
//Do stuff with result
}
});
})
.then(function() {
init();
});
defer.resolve();
The promise chaining you are looking for only works if you are returning a promise again in any then block. If you don't return a promise, the then handle will immediately return undefined and subsequent handlers will be called instantly. If however, you return a promise, the next then handler will wait for this promise to be resolved and so on.
Also, it looks like your methods getColumnSettings and getDefaultGroupedBy are already returning promises, so instead of wrapping them in a deferred object you might as well use them right away. If, however, you do not exactly know, how the promises returned by SignalR behave, you can still wrap them using the Angular's $q api.
You should be able to write something like:
var columnSettingsPromise = $q(function(resolve, reject) {
myHub.server.getColumnSettings().done(function (result) {
if (result) {
// Do stuff with result
// resolve the promise with the obtained result (will be passed to the then handler)
resolve(result);
// we are returning a promise in this function which will be resolved at some point
} else {
reject(new Error('no column settings loaded'));
}
});
});
// wait until the column settings have been retrieved
columnSettingsPromise.
then(function(columnSettings) {
// return a new promise, the next then handler will wait for this promise
return $q(function(resolve, reject) {
myHub.server.getDefaultGroupedBy().done(function(result) {
if (result) {
// do stuff with the result
resolve(result);
} else {
reject(new Error('no default grouped by data loaded'));
}
});
});
})
// the next handler will only be called after the promise for getDefaultGroupedBy data has been resolved
// as soon as that's the case, just call init
.then(init);
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.
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);
});