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);
Related
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"?
I'm trying to test what the following method returns in it's promise (functionToTest and asyncGet are both methods defined in an angularJS service):
var functionToTest = function (param) {
var deferred = $q.defer();
asyncGet(param).then(function (result) {
//business login involving result
if (something)
return deferred.resolve(true);
else
return deferred.resolve(false);
});
return deferred.promise;
}
The unit test looks like this:
it ('should return true', function (done) {
var asyncGetResult = {};
spyOn(asyncGet).and.returnValue($q.resolve(asyncGetResult));
var param = {};
functionToTest(param).then(function (result) {
expect(result).toBe(true);
done();
});
$scope.$apply();
});
When running the test I am getting a timeout error:
Async callback was not invoked within timeout specified by
jasmine.DEFAULT_TIMEOUT_INTERVAL.
I tried putting a console.log() right after the expect() but it does not print anything, so it seems like the callback for the functionToTest(param).then() is never executed.
Any help would be appreciated.
Remove the $q.deferred anti-pattern:
var functionToTest = function (param) {
̶v̶a̶r̶ ̶d̶e̶f̶e̶r̶r̶e̶d̶ ̶=̶ ̶$̶q̶.̶d̶e̶f̶e̶r̶(̶)̶;̶
return asyncGet(param).then(function (result) {
//business login involving result
if (something)
return true;
else
return false;
});
̶r̶e̶t̶u̶r̶n̶ ̶d̶e̶f̶e̶r̶r̶e̶d̶.̶p̶r̶o̶m̶i̶s̶e̶;̶
}
Also provide an error handler:
functionToTest(param).then(function (result) {
expect(result).toBe(true);
done();
}).catch(function(err) {
console.log(err);
done();
});
Deferred anti-patterns can hang if the code is written erroneously. Avoid the anti-pattern to prevent this problem.
Include a .catch handler to see any errors.
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 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 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