Promise chains don't work as expected - javascript

With the code below, my controller's publish() would always go to createCompleted() even if the server returned 500. I was under impression that catch() would be executed when 400 or 500 codes are returned from the server.
// in service
function create(item) {
return $http
.post(api, item)
.then(createCompleted)
.catch(createFailed);
function createCompleted(response) {
return response.data;
}
function createFailed(error) {
$log.error('XHR Failed for create: ' + error.data);
}
}
// in controller
function publish(item) {
item.published = true;
return itemService.create(item)
.then(createCompleted)
.catch(createFailed);
function createCompleted(response) {
alertService.add('success', 'success.');
$state.go('^');
}
function createFailed(error) {
alertService.add('error', 'failed');
}
}
While the controller's createFailed() doesn't hit, the service createFailed() always hits.
What is going on here?

Well that is because you are not propagating the error properly. you would need to throw an exception or reject explicitly from createFailed function.
function createFailed(error) {
$log.error('XHR Failed for create: ' + error.data);
throw error;
// or
return $q.reject(error); // inject $q
}
So in your case since you are not returning anything it is assumed to be resolved from the returned promise with the value "undefined".

Related

How to cancel a promise with $q in angular js

I have a service below. I will call this service every time when I open a model and when I close the model and then open another one the previous values are getting reflected and in this case I want to cancel the promise every time I close the model.
I have tried the following code,
Model closing.js
$scope.closeButton = function() {
DetailDataSvc.storeDefer().resolve()
}
My Service, (DetailDataSvc)
self.storeDefer = function() {
return self.deferReturn;
};
self.getDetailReportData = function(postData, functionName) {
var promises = {};
var d = $q.defer(),
metricDataType;
self.deferReturn = $q.defer();
promises = {
detailReport: metricDataType,
recommendedMetrics: DataSvc.getData(_logPrefix + functionName, recommendedMetricUrl),
metricInfo: DataSvc.getData(_logPrefix + functionName, metricInfoUrl)
};
$q.all(promises).then(function(res) {
$log.debug(_logPrefix + 'getDetailReportData(). Called from %s. $q.all Response (raw): ', functionName, res);
else {
if (response && !_.isEmpty(_.get(response, 'largeCard.chartData.dataValues.rows')) && response.overlayEnabled) {
self.getMetricOverLay(pdata, functionName).then(function(overlayData) {
response.largeCard.chartData.overlay = overlayData;
d.resolve(response);
}, function(msg, code) {
d.reject(msg);
$log.error(_logPrefix + 'getDetailReportData(). Error code: %s. Error: ', code, msg);
});
} else {
d.resolve(response);
}
}
}, function(msg, code) {
d.reject(msg);
$log.error(_logPrefix + 'getDetailReportData(). Error code: %s. Error: ', code, msg);
});
return d.promise;
};
Can anyone please help me whether the process I followed is the right one.
What you have attempted could be made to work but it's best fixed by racing the promise returned by $q.all() against a rejectable Deferred (ie. a Deferred, of which a reference is kept to its reject method), thus avoiding the deferred anti-pattern.
self.getDetailReportData = function(postData, functionName) {
var metricDataType = ......; // ???
var d = $q.defer();
// cancel previous
if(self.cancelDetailReport) {
self.cancelDetailReport(new Error('previous getDetailReportData() cancelled'));
}
// keep a reference to the deferred's reject method for next time round.
self.cancelDetailReport = d.reject;
var promises = {
'detailReport': metricDataType,
'recommendedMetrics': DataSvc.getData(_logPrefix + functionName, recommendedMetricUrl),
'metricInfo': DataSvc.getData(_logPrefix + functionName, metricInfoUrl)
};
// Race aggregated `promises` against `d.promise`, thus providing the required cancellation effect.
return $q.race([$q.all(promises), d.promise])
.then(function(response) {
// arrive here only if all promises resolve and d.reject() has not been called.
$log.debug(_logPrefix + 'getDetailReportData(). Called from %s. $q.all Response (raw): ', functionName, response);
if (response && !_.isEmpty(_.get(response, 'largeCard.chartData.dataValues.rows')) && response.overlayEnabled) {
return self.getMetricOverLay(pdata, functionName)
.then(function(overlayData) {
response.largeCard.chartData.overlay = overlayData;
return response;
});
} else {
return response;
}
})
.catch(function(msg, code) { // signature?
// all error cases including cancellation end up here.
var message = _logPrefix + `getDetailReportData(). Error: (${code}): ${msg}`; // or similar
$log.error(message);
throw new Error(message); // see https://stackoverflow.com/a/42250798/3478010
});
};
Notes:
$q.race() is transparent to whichever promise wins the race, and opaque to the other. So, if the d is rejected before the promise returned by $q.all() settles, then d will win out; response handling will not happen and d's rejection will fall through to the .catch() clause. Alternatively, if the promise returned by $q.all(promises) wins out then flow will follow that promise's success path (ie response handling) or possibly its error path (which will drop through to the .catch() clause).
Not too sure about the signature of the .catch() callback. You would normally expect it to accept a single error argument.
Assign already created deferred.
Try and change this line:
self.deferReturn = $q.defer();
self.deferReturn = d;

Global Error handler that only catches "unhandled" promises

I have a global error handler for my angular app which is written as an $http interceptor, but I'd like to take it a step further. What I'd like is for each $http call that fails (is rejected), any "chained" consumers of the promise should first try to resolve the error, and if it is STILL unresolved (not caught), THEN I'd like the global error handler to take over.
Use case is, my global error handler shows a growl "alert box" at the top of the screen. But I have a couple of modals that pop up, and I handle the errors explicitly there, showing an error message in the modal itself. So, essentially, this modal controller should mark the rejected promise as "handled". But since the interceptor always seems to be the first to run on an $http error, I can't figure out a way to do it.
Here is my interceptor code:
angular.module("globalErrors", ['angular-growl', 'ngAnimate'])
.factory("myHttpInterceptor", ['$q', '$log', '$location', '$rootScope', 'growl', 'growlMessages',
function ($q, $log, $location, $rootScope, growl, growlMessages) {
var numLoading = 0;
return {
request: function (config) {
if (config.showLoader !== false) {
numLoading++;
$rootScope.loading = true;
}
return config || $q.when(config)
},
response: function (response) {
if (response.config.showLoader !== false) {
numLoading--;
$rootScope.loading = numLoading > 0;
}
if(growlMessages.getAllMessages().length) { // clear messages on next success XHR
growlMessages.destroyAllMessages();
}
return response || $q.when(response);
},
responseError: function (rejection) {
//$log.debug("error with status " + rejection.status + " and data: " + rejection.data['message']);
numLoading--;
$rootScope.loading = numLoading > 0;
switch (rejection.status) {
case 401:
document.location = "/auth/login";
growl.error("You are not logged in!");
break;
case 403:
growl.error("You don't have the right to do this: " + rejection.data);
break;
case 0:
growl.error("No connection, internet is down?");
break;
default:
if(!rejection.handled) {
if (rejection.data && rejection.data['message']) {
var mes = rejection.data['message'];
if (rejection.data.errors) {
for (var k in rejection.data.errors) {
mes += "<br/>" + rejection.data.errors[k];
}
}
growl.error("" + mes);
} else {
growl.error("There was an unknown error processing your request");
}
}
break;
}
return $q.reject(rejection);
}
};
}]).config(function ($provide, $httpProvider) {
return $httpProvider.interceptors.push('myHttpInterceptor');
})
This is rough code of how I'd expect the modal promise call to look like:
$http.get('/some/url').then(function(c) {
$uibModalInstance.close(c);
}, function(resp) {
if(resp.data.errors) {
$scope.errors = resp.data.errors;
resp.handled = true;
return resp;
}
});
1. Solution (hacky way)
You can do that by creating a service doing that for you. Because promises are chain-able and you basically mark a property handled at the controller level, you should pass this promise to your service and it'll take care of the unhandled errors.
myService.check(
$http.get('url/to/the/endpoint')
.then( succCallback, errorCallback)
);
2. Solution (preferred way)
Or the better solution would be to create a wrapper for $http and do something like this:
myhttp.get('url/to/the/endpoint', successCallback, failedCallback);
function successCallback(){ ... }
function failedCallback(resp){
//optional solution, you can even say resp.handled = true
myhttp.setAsHandled(resp);
//do not forget to reject here, otherwise the chained promise will be recognised as a resolved promise.
$q.reject(resp);
}
Here the myhttp service call will apply the given success and failed callbacks and then it can chain his own faild callback and check if the handled property is true or false.
The myhttp service implementation (updated, added setAsHandled function which is just optional but it's a nicer solution since it keeps everything in one place (the attribute 'handled' easily changeable and in one place):
function myhttp($http){
var service = this;
service.setAsHandled = setAsHandled;
service.get = get;
function setAsHandled(resp){
resp.handled = true;
}
function get(url, successHandler, failedHandler){
$http.get(url)
.then(successHandler, failedHandler)
.then(null, function(resp){
if(resp.handled !== true){
//your awesome popup message triggers here.
}
})
}
}
3. Solution
Same as #2 but less code needed to achieve the same:
myhttp.get('url/to/the/endpoint', successCallback, failedCallback);
function successCallback(){ ... }
function failedCallback(resp){
//if you provide a failedCallback, and you still want to have your popup, then you need your reject.
$q.reject(resp);
}
Other example:
//since you didn't provide failed callback, it'll treat as a non-handled promise, and you'll have your popup.
myhttp.get('url/to/the/endpoint', successCallback);
function successCallback(){ ... }
The myhttp service implementation:
function myhttp($http){
var service = this;
service.get = get;
function get(url, successHandler, failedHandler){
$http.get(url)
.then(successHandler, failedHandler)
.then(null, function(){
//your awesome popup message triggers here.
})
}
}

Prevent AngularJS $http return on timeout

I am doing custom $http service that looks something like this:
angular.factory('myHttp', function($http){
var obj = {};
obj.get = function(path) {
return $http.get(path,{timeout: 5000}).error(function (result) {
console.log("retrying");
return obj.get(path);
});
}
});
The system works fine. It does return the data when success, and retrying when connection fail. However, I am facing problem that it will return to controller when the connection is timeout. How can I prevent it from returning and continue retrying?
You need to use $q.reject. This will indicate that the error handler failed again and the result should populated to the parent error handler - NOT the success handler.
obj.get = function(path) {
return $http.get(path, {
timeout: 5000
}).then(null, function(result) {
console.log("retrying");
if (i < retry) {
i += 1;
return obj.get(path);
} else {
return $q.reject(result); // <-- use $q.reject
}
});
}
See plunker
See the reject.status to determine the timeout
$http.get('/path', { timeout: 5000 })
.then(function(){
// Your request served
},function(reject){
if(reject.status === 0) {
// $http timeout
} else {
// response error
}
});
Please see the following question for a good overview about handling timeout errors:
Angular $http : setting a promise on the 'timeout' config

chaining promise error handler

Please see the demo here
function get(url) {
return $http.get(url)
.then(function(d){
return d.data
},
function(err){ //will work without handling error here, but I need to do some processing here
//call gets here
//do some processing
return err
})
}
get('http://ip.jsontest.co')
.then(function(response){
$scope.response = "SUCCESS --" + JSON.stringify(response);
}, function(err){
$scope.response = "ERROR -- " + err;
})
I have a library function, get, which returns a promise. I am processing the error there, and returns it (where I commented //do some processing ). I was expecting in the client, it calls the error/fail handler. instead it prints "SUCCESS --" + error
I can make this work with $q and reject, but is there a way without?
Generally:
Whenever you return from a promise handler, you are resolving indicating normal flow continuation.
Whenever you throw at a promise handler, you are rejecting indication exceptional flow.
In a synchronous scenario your code is:
function get(url){
try{
return $http.get(url);
} catch(err){
//handle err
}
}
If you want to pass it further, you need to rethrow:
function get(url){
try{
return $http.get(url);
} catch(err){
//handle err
throw err;
}
}
Promises are exactly like that:
function get(url){
return $http.get(url)
.then(function(d){
return d.data
},
function(err){ //will work without handling error here
//call gets here
//do some processing
throw err; // note the throw
})
};
Or with even niftier syntax:
function get(url){
return $http.get(url).catch(function(err){
// do some processing
throw err;
});
}
Replace return err with $q.reject(err), you need to inject $q of course.
In promise chaining, if you want to pass the error down, you'll need to return a rejected promise from current error handler. Otherwise, if the return value is an immediate value or resolved promise, the error is considered to be handled, so later error handlers down the chain won't be called.

Angular Response Interceptor--Handling Promise

I have created the interceptor below--it basically redirects to a known location when the service sends a response indicating that the user's session has expired.
It currently works correctly--what I'm not sure about is whether to reject the promise, return the response, or do something different. If I just redirect, it works as expected. If I reject the promise, I end up in the error handler of the ajax call (not shown), but it otherwise successfully redirects.
What is the correct way to fulfill the promise in this scenario?
Edit: Added the else clause.
var responseInterceptor = function ($q) {
return function (promise) {
return promise.then(function (response) {
if (response.data.SessionHasExpired == true) {
window.location.href = "/Home/Login?message=User session has expired, please re-login.";
return $q.reject(response); //do I need this? What to do here?
}
else {
return response;
}
}, function (response) {
return $q.reject(response);
});
};
};
In such a case, I think you should handle the error inside the error callback, and only reject the promise if the error isn't something you were expecting. I mean, deal with the session timeout error and reject the promise for everything else. If you reject it even after handling the error, all errors callbacks related to the promise will be invoked (as you've noticed yourself) and none of them will handle the session timeout error (after all, you have made an interceptor for doing precisely that).
I support the #idbehold advice of using a more appropriate status code for this situation. 401 Unauthorized is the way to go.
With all of this being considered, your code could look like this:
var responseInterceptor = function($q) {
return function(promise) {
var success = function(response) {
return response;
};
var error = function(response) {
if (response.status === 401) {
window.location.href = "/Home/Login?message=User session has expired, please re-login.";
}
else {
return $q.reject(response);
}
};
return promise.then(success, error);
};
};
Perhaps you'd be interested in checking out this Angular module on Github.

Categories

Resources