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.
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.
I'm trying to get back a new ID of a saved record, then save that id into an array to another object. However, the ID never comes back in time or does as undefined. Im pretty new to angular and firebase and have the current code so far.
Service:
function addNewOffer(uid, offer) {
var deferred = $q.defer();
offers = $firebaseArray(firebaseDataService.offers);
offers.$loaded().then(function (data) {
offers = data;
offers.$add(offer).then(function (ref) {
var offerid = ref.key;
console.log("added record with id " + offerid);
deferred.resolve(offerid);
return deferred.promise;
}).catch(function (err) {
console.log(err);
deferred.reject(error);
});
})
}
Controller:
var offerid = offerService.addNewOffer(vm.userid, vm.offer);
console.log(offerid);
vm.transaction.offers.unshift(offerid);
//then save transaction here
console says offerid is undefined. So, then making the transaction not save.
You almost had it. You need to grab the promise. The first function is the resolve function, the second is the reject function where you handle the error response from addNewOffer().
offerService.addNewOffer(vm.userid, vm.offer).then(function(offerid) {
console.log(offerid);
vm.transaction.offers.unshift(offerid);
}, function(err) {
console.log(err);
})
You should also move the return statement outside of the success handler in your main function:
function addNewOffer(uid, offer) {
var deferred = $q.defer();
offers = $firebaseArray(firebaseDataService.offers);
offers.$loaded().then(function (data) {
offers = data;
offers.$add(offer).then(function (ref) {
var offerid = ref.key;
console.log("added record with id " + offerid);
deferred.resolve(offerid);
}).catch(function (err) {
console.log(err);
deferred.reject(error);
});
});
return deferred.promise;
}
Otherwise you won't have a value until the firebase promise is resolved.
Since the AngularFire API returns promises, there is no need to manufacture a promise with $q.defer
Promises are chained by using a return statement in the success handler. Rejections are chained by using a throw statement.
function addNewOffer(uid, offer) {
//var deferred = $q.defer();
var offers = $firebaseArray(firebaseDataService.offers);
promise = offers.$loaded()
.then(function (data) {
var offers = data;
//return promise to chain
return offers.$add(offer);
}).then(function (ref) {
var offerid = ref.key;
console.log("added record with id " + offerid);
//deferred.resolve(offerid);
//return deferred.promise;
//return promise to chain
}).catch(function (err) {
console.log(err);
//deferred.reject(error);
//throw to chain error
throw err;
});
return promise;
}
Controller
var offeridPromise = offerService.addNewOffer(vm.userid, vm.offer);
offeridPromise
.then(function(offerid) {
console.log(offerid);
//vm.transaction.offers.unshift(offerid);
//then save transaction here
return offerid;
}).catch(function(err) {
console.log(err);
throw err;
});
Chaining promises
Because calling the .then method of a promise returns a new derived promise, it is easily possible to create a chain of promises. It is possible to create chains of any length and since a promise can be resolved with another promise (which will defer its resolution further), it is possible to pause/defer resolution of the promises at any point in the chain. This makes it possible to implement powerful APIs
-- AngularJS $q Srevice API Reference - Chaining Promises
I built a factory to return data that uses an HTTP Get through a deferred promise. It work great when it is the happy path and the url is correct. But when there is an error I would like to catch it. It seems that I am but a 500 error still shows in the console. Is there a way to catch this also? Also, I want to do processing on the reject I'm having trouble figuring out how to do that. TIA
angular.module("accQueries")
.factory('leaseFactory', ['$http', '$q', function ($http, $q) {
return {
leases: '',
makeRequest: function (url) {
// Create the deferred object
var deferred = $q.defer();
$http.get(url).then(function (resp) {
deferred.resolve(resp.data);
})
// potentially catch http error here??
.catch(function (err) {
deferred.reject(err);
console.log('rejected : ' + err );
console.dir(err);
this.leases = '';
});
return deferred.promise;
},
// Return a single lease based on lease number
getLease: function (pLeaseNum) {
this.leases = this.makeRequest("http://someserver/AccruentQA_DB/webresources/restfulservices.latbllease/leaseNumber/" + pLeaseNum);
// Return the lease object stored on the service
return this.leases;
},
// Return all leases based on lease name
getLeases: function () {
this.leases = this.makeRequest("http://someserver/AccruentQA_DB/webresources/restfulservices.latbllease/name/");
// Return the lease object stored on the service
return this.leases;
}
};
}]);
It is not needed to wrap a $http call in $q, because $http returns a promise itself. So just returning $http like this is sufficient:
makeRequest: function (url) {
return $http.get(url);
}
If you would want to chain do something in the makeRequest function with the answers be4 passing it on, you can chain promises like so:
makeRequest: function (url) {
return $http.get(url).then(function(response){
//do something
return response;
}, function(error){
//do something
return error;
});
}
There's no way to prevent the HTTP error from appearing in the console. The browser does that before it passes the results back to angular. However, an error causes the $http promise to be rejected, which means you can handle it using the optional second argument to then()
return $http.get('url').then(
function(response) {
this.leases = response.data;
},
function(response) {
var statusCode = response.status;
var response = response.data;
// other error processing
this.leases = '';
}
}).then(function() { return this.leases; }
You can do various things depending on the status code and response data. If your server emits an error 500, that's what response.status will be. Timeouts have a status of 0.
You should also be aware that getLease() will return before the ajax request is complete. You should return the promise, and then in the calling code, chain another then() to do something once the promise is resolved.
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);
});
});
};
Hello everyone :) Here is the problem. I'm making an angular app with:
a factory to access to an api with $http that retrieves an array of objects from a server
getObjectsFromApi : function(){
return $http({
url: 'http://path/to/the/api/',
method: 'GET',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
}
})
},
and a service to compute retrieved data and inject it to controllers
this.getObjectsFromService = function(){
var objects = [];
ObjectFactory.getObject()
.success(function(data, status){
console.log("Successfully got objects !");
objects = data;
})
.error(function(data, status){
console.log("Fail... :-(");
});
return objects;
};
The problem is that when I return objects, it doesn't return any data. How can I do to return $http callback data within this getObjectsFromService function ?
Thank you for helping !
You must use promises, something like this should do the trick
this.getObjectsFromService = function(){
var defer = $q.defer();
ObjectFactory.getObject()
.then(function(data){
console.log("Successfully got objects !");
defer.resolve(data);
})
.catch(function(data){
console.log("Fail... :-(");
defer.reject(data);
});
return defer.promise;
};
And now you can use this function somewhere else like this:
var foo = function() {
var objects = [];
this.getObjectsFromService().then(function(data) {
objects = data;
//do rest of your manipulation of objects array here
});
}
There is no other way to return the objects, it's not possible because $http is asynchronous. You'll have to rework the rest of the code and adapt it to this
var req = ObjectFactory.getObject();
req.success(function(data) {...}
req.error(function(data) {...}
This will do it for ya
The variable req will then be the promise sent back from the factory.
Edit
Keep in mind that your data will not be changes until the promise is resolved, so if you try to console.log(objects) before then, it will be empty.
The http request is async, which means that it completes after you return objects. Return the promise instead:
this.getObjectsFromService = function(){
return ObjectFactory.getObject().catch(function(){
console.log("Fail... :-(");
});
}
Then
service.getObjectsFromService().then(function(resp) {
console.log("Successfully got objects !", resp.data);
});
Your code is asynchronous. When you return objects, you return your initial empty array. You can't return directly your object, but instead you need to return a promise (see 3 possibilities below)
By creating a promise
var deferred = $q.defer();
ObjectFactory.getObjectsFromApi()
.success(function(data, status){
console.log("Successfully got objects !");
// Do stuff and resolve promise
deferred.resolve(data);
})
.error(function(data, status){
console.log("Fail... :-("));
// Do stuff and reject promise
deferred.reject(data)
});
return deferred.promise;
You can use promise chaining (use .then rather than .success and .error):
Note: when using then with success and error callbacks rather than success and error methods, you have only 1 argument which is the response object
return ObjectFactory.getObjectsFromApi()
.then(function(response){
console.log("Successfully got objects !");
// Do stuff and chain full response headers or data
return responseHeaders;
// or
// return responseHeaders.data;
}, function(responseHeaders){
console.log("Fail... :-("));
// Do stuff and chain error (full response headers or data)
return $q.reject(responseHeaders)
// return $q.reject(responseHeaders.data);
});
Or if you have no business logic, or no reason to intervene in your factory, simply return your $http call directly:
return ObjectFactory.getObjectsFromApi();
Angular $resource
Factory
getObjectsFromApi : function(){
return $resource('http://path/to/the/api/', {},
{
get: {
method: 'GET',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
}
}
})
},
Service
this.getObjectsFromService = function(){
var objects = [];
ObjectFactory.get().$promise.then(function(data) {
objects = data;
return objects;
}).error(function(err) {
throw err;
});
};