I have written a function wrapper that returns cached values for HTTP responses. In a specific situation (marked by comment // <--HERE) I see inconsistent behavior. I'm frankly not sure what exactly the inconsistency is, but bottom line, when the cache expires (has_expired), it does not wait for the http get to return in the recursive call.
My guess is I haven't put a "return" somewhere on a promise but I can't find out where (and why). Do I need to put a return in front of localForage.removeItem (and if so why?)
function cache_or_http(url,key) {
if (dont_use_cache()) {
return $http.get(url);
}
var d = $q.defer();
localforage.getItem(key)
.then (function(data) {
if (data) { // exists
if (has_expired(data.created_at)) {
localforage.removeItem(key)
.then (function() {return cache_or_http(url,key);}) // <--HERE
.catch(function() {return do_error_handling();})
} else { // not expired
d.resolve(JSON.parse(data.value));
return d.promise;
}
} else {
// doesn't exist
return $http.get(url)
.then (function(data) {
cache_entry = {
'value': JSON.stringify(data),
'created_at': moment().toString()
};
localforage.setItem(key, cache_entry);
d.resolve(data);
return (d.promise);
});
} // doesn't exist
}); // getItem .then
return (d.promise);
}
There is no need to manufacture a new promise with $q.defer. The .then method of a promise already returns a promise.
function cache_or_http(url,key) {
̶v̶a̶r̶ ̶d̶ ̶=̶ ̶$̶q̶.̶d̶e̶f̶e̶r̶(̶)̶;̶
̲r̲e̲t̲u̲r̲n̲ localforage.getItem(key)
.then (function(data) {
if (data) { // exists
if (has_expired(data.created_at)) {
̲r̲e̲t̲u̲r̲n̲ localforage.removeItem(key)
.then (function() {return cache_or_http(url,key);}) // <--HERE
.catch(function() {return do_error_handling();})
} else { // not expired
̶d̶.̶r̶e̶s̶o̶l̶v̶e̶(̶J̶S̶O̶N̶.̶p̶a̶r̶s̶e̶(̶d̶a̶t̶a̶.̶v̶a̶l̶u̶e̶)̶)̶;̶
return JSON.parse(data.value);
}
} else {
// doesn't exist
return $http.get(url)
.then (function(data) {
cache_entry = {
'value': JSON.stringify(data),
'created_at': moment().toString()
};
̲r̲e̲t̲u̲r̲n̲ localforage.setItem(key, cache_entry);
̶d̶.̶r̶e̶s̶o̶l̶v̶e̶(̶d̶a̶t̶a̶)̶;̶
̶r̶e̶t̶u̶r̶n̶ ̶(̶d̶.̶p̶r̶o̶m̶i̶s̶e̶)̶;̶
});
} // doesn't exist
}); // getItem .then
̶r̶e̶t̶u̶r̶n̶ ̶(̶d̶.̶p̶r̶o̶m̶i̶s̶e̶)̶;̶
}
For more information, see
Is this a "Deferred Antipattern"?
Related
I have the problem that my function doesn't wait for the response of http request and go further. I know that I can use promise to wait but I don't understand the concept.
I have a data service that have all http request :
function GetGroupIdFromBakery(bakeryId, successCallback, errorCallback) {
$http.get(service.baseUrl + "BakeriesGroup/Bakeries/" + bakeryId)
.then(function (result) { successCallback(result.data); }, errorCallback);
}
From another service, I call the data service :
var hasPermission = function (permission, params) {
permissionRoute = permission;
setIdEntity(params);
for (var i = 0; i < permissions.length; i++) {
if (permissionRoute.Name === permissions[i].Name) {
if (permissions[i].Scope == "System")
return true;
else if (permissions[i].Scope == permissionRoute.Scope && permissions[i].IdEntity == permissionRoute.IdEntity)
return true;
}
}
return false;
}
var setIdEntity = function (params) {
if (permissionRoute.Scope == "Bakery")
permissionRoute.IdEntity = parseInt(params.bakeryId);
else if (permissionRoute.Scope == "Group") {
if (params.bakeriesGroupId)
permissionRoute.IdEntity = parseInt(params.bakeriesGroupId);
else {
getGroupOfBakery(parseInt(params.bakeryId));
}
console.log(permissionRoute.IdEntity);
}
}
var getGroupOfBakery = function (bakeryId) {
DataService.GetGroupIdFromBakery(bakeryId, function (groupId) {
permissionRoute.IdEntity = groupId;
}, function (error) {
console.error("something went wrong while getting bakery");
alert("Une erreur s'est produite lors de la récupération de la boulangerie");
});
}
I must wait for the response of DataService.GetGroupIdFromBakery(). With this code, permission.EntityId is undefined when I call getGroupByBakery().
Can somebody help me, please?
You can add a watcher to your response data. I think it is EntityId in your case.
It get executed as soon as your EntityId changes. After getting the response data you can call the function, this time EntityId will not be undefined.
$scope.$watch(function () {
return EntityId
}, function (newEntityId) {
if(newEntityId != undefined {
// now you can call your function
}
}
}, true);
Exactly, you have to use promises, because $http module is asynchronus. I built a service for that:
.service('RequestService', function($q, $http){
return {
call: function(htmlOptions){
var d = $q.defer();
var promise = d.promise;
$http(htmlOptions)
.then(function(response){
d.resolve(response.data);
}, function(response){
d.reject(response.data);
});
promise.success = function(fn) {
promise.then(fn);
return promise;
};
promise.error = function(fn) {
promise.then(null, fn);
return promise;
};
return promise;
}
}
})
And then:
RequestService.call({
method: 'POST' //GET, DELETE ...
data: data,
url: 'http://someurl.com/'
});
you should use defer.
create a new defer object in GetGroupIdFromBakery method,in success part resolve the defer and in failed part reject it, and return the promise of defer at the end.
function GetGroupIdFromBakery(bakeryId, successCallback, errorCallback, $q) {
var defer = $q.defer();
$http.get(service.baseUrl + "BakeriesGroup/Bakeries/" + bakeryId)
.then(function (result) {
successCallback(result.data);
defer.resolve(result.data);
}, function(){
defer.reject();
});
return defer.promise;
}
This successfully return you a promise that you can call with .then() and receive as a value in the service where you need to have the data of GetGroupIdFromBakery.
getGroupOfBakery(parseInt(params.bakeryId), $q).then(function(data){
// here you have data
})
Be aware you should inject $q, here I supposed that we have $q in service and I passed it to method.
I am getting the error Cannot read property of '$promise' of undefined.
Here is the code that is throwing it:
var myPromise = sharedDataService.getData($scope.entityId).$promise;
resolvePromise(myPromise, 'entityData');
the resolvePromise method:
function resolvePromise(promise, resultObject){
promise.then(function(response){
$scope[resultObject] = result;
});
promise['catch'](function(error){
//error callback
});
promise['finally'](function(){
//finally callback
});
sharedDataService looks like this:
var publicInterface = {
getData: getData
};
var storedData;
function getData(entityId) {
if(storedData.entityId === entityId){
return storedData;
}else{
var entityDataPromise = dataAccessService.getEntityData(entityId).$promise;
entityDataPromise.then(function (response) {
storedData = response;
return storedData ;
});
entityDataPromise['catch'](function(error) {
//throw error;
});
entityDataPromise['finally'](function(done) {
//do finally
});
}
}
return publicInterface;
finally, the dataAccessService:
var publicInterface = {
getEntityData: getEntityData
}
var entityData = $resource(apiUrl + 'Entity', {}, {
'getEntityData': {
method: 'GET',
url: apiUrl + 'Entity/getEntityDataById'
}
}
function getEntityData(entityId){
return entityData.getEntityData({entityId: entityId})
}
return publicInterface;
the original promise is throwing an error. When I put breakpoints various places, I can see my data is being returned sometimes. The functionality of sharedDataService is almost one of a chaching service.
Why is my original promise returning undefined?
Your getData() method doesn't have a return when if is false. So you would need to return entitiyDataPromise.
But, that would mean one condition returns a promise and the other returns an object
So both conditions need to return a promise and we can use $q for the first condition
function getData(entityId) {
if(storedData.entityId === entityId){
// return to getData()
return $q.resolve(storedData);
}else{
var entityDataPromise = dataAccessService.getEntityData(entityId).$promise;
// return to getData()
return entityDataPromise.then(function (response) {
storedData = response;
return storedData ;
});
entityDataPromise['catch'](function(error) {
//throw error;
});
entityDataPromise['finally'](function(done) {
//do finally
});
}
}
Be sure to inject $q in service.
In controller would be:
var myPromise = sharedDataService.getData($scope.entityId);
resolvePromise(myPromise, 'entityData');
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);
Goodmorning,
i wanted to know if there's a way to attach by default a promises, i'll explain better....
module.factory('serviceName', function($http) {
$http.defaults.headers.post["Content-Type"] = "application/x-www-form-urlencoded";
return {
call1: function(callback) {
return $http.post('url1/something').then(callback);
},
call2: function(param1, param1, callback) {
return $http.post('url2/something', $.param({param1: param1, param2: param2, })).then(callback);
}
};
});
this is basically my service and i would like to know if there's a way of chaining a promise at the end of each of this call using something like $http.default.something
this would be really helpful :D
if there's a way of chaining a promise
Yes,
You can chain promises to create code flows
One of advantages: Error propagates, so you can catch it on the end of the
chain
Reference how chain promises works
About your example:
I don't think its a good way to use callback when $http.post returns promise itself.
If you want to create factory return promise, you can write:
var request = function(data) {
var deferred = $q.defer();
var configHttp = {
method: 'POST',
url: 'some URL'
};
if (data !== undefined) {
configHttp.data = data;
}
$http(configHttp).success(function(data, status, headers) {
if (data.error === undefined) {
deferred.resolve(data);
} else {
deferred.reject(data);
}
}).error(function(data, status, headers) {
deferred.reject(data);
});
return deferred.promise;
}
return {
call1: function(param1) {
return request('get_1', {param: param1});
},
call2: function(param2) {
return request('get_2', {param: param2});
}
So in controller we can write:
serviceName.call1..then(function(result)
{
//....
}, function(error) {
alert(error.message);
});