The deferred antipattern, how to handle rejects? - angularjs

I understand what is The deferred antipattern, also mentioned as "The Forgotten Promise" HERE.
I also read: what-is-the-explicit-promise-construction-antipattern-and-how-do-i-avoid-it.
Anyways I should try to get rid of using $q.defer().
But I don't understand how to handle case where I got response and I need to reject it because it doesn't have proper key.
This is my method in Service that returns promise to controller.
self.findTime = function(data){
var deferred = $q.defer();
apiClient.findTime(data)
.then(function (response) {
if (response.result !== "success") {
deferred.reject(response);
}
deferred.resolve(response);
}
, function (error) {
deferred.reject(error);
console.error(error);
});
return deferred.promise;
};
So I tried to change it to:
self.findTime = function(data){
return apiClient.findTime(data)
.then(function (response) {
if (response.result !== "success") {
// return WHAT ??? <-------------
}
return response;
}
, function (error) {
// return WHAT ??? <-------------
console.error(error);
});
};
How to handle reject?
In controller, on reject I show some morning message.
Service.findTime(data).then(function (response) {
// do something
}, function (error) {
// show warn dialog
});

You have two options.
You can return a rejection promise from within your resolve:
return apiClient.findTime(data)
.then(function (response) {
if (response.result !== "success") {
return $q.reject('reason'); // Convert to rejection
}
return response;
}, function (error) {
console.error(error);
return $q.reject('reason'); // Chain rejection
});
or you can throw an exception:
return apiClient.findTime(data)
.then(function (response) {
if (response.result !== "success") {
throw new Error('reason');
}
return response;
}, function (error) {
console.error(error);
throw new Error(error);
});

Related

Convert promise in angular 1 to observable in angular 2

i'm still learning observable in angular 2, and has not been able to figure out how to convert my code in angular 1 to angular 2.
function promiseFunc() {
var deferred = $q.defer();
$http.post(url, something)
.then(function (response) {
if (response === 1) deferred.resolve(response.data);
else deferred.reject();
}).catch(function (e) {
deferred.reject(e);
});
return deferred.promise;
}
Can anyone tell me how i can convert this code with angular 2 observable?
EDIT:
And what if the http.post is optional?
function promiseFunc(param1) {
var deferred = $q.defer();
if (param1 === 1) {
deferred.resolve(1);
} else {
$http.post(url, something)
.then(function (response) {
if (response.x === 1) deferred.resolve(response);
else deferred.reject();
}).catch(function (e) {
deferred.reject(e);
});
}
return deferred.promise;
}
What i'm missing in observable is the ability to call resolve and reject. Can it be done in observable?
someMethod() {
if(param === 1) {
return Observable.of(1);
} else {
return this.http.post(url, something)
.map(
response => {
let data = response.json();
if(data === 1) {
return 1;
}
throw 'some error';
}
);
}
}
then use it like
this.someMethod().subscribe(
data => console.log(data),
error => console.log(error),
() => console.log('completed')
);

Getting Cannot read property 'then' of undefined when using .fromFnPromise in angular-datatables

//the controller that creates the datatable
app.controller('AdminListCtrl', function ($scope, $compile, DTOptionsBuilder, DTColumnBuilder, adminService) {
var vm = this;
function stateChange(iColumn, bVisible) {
console.log('The column', iColumn, ' has changed its status to', bVisible);
}
//vm.dtOptions = DTOptionsBuilder.fromSource('http://localhost/api-v1/admin')
vm.dtOptions = DTOptionsBuilder.fromFnPromise(function() {
return adminService.loadAdmin();
})
.withPaginationType('full_numbers')
.withOption('createdRow', createdRow)
// Add Bootstrap compatibility
.withBootstrap()
// Active ColVis plugin
.withColVis()
// Add a state change function
.withColVisStateChange(stateChange)
// Exclude the last column from the list
.withColVisOption('aiExclude', [2])
// Add Table tools compatibility
.withTableTools('scripts/vendor/datatables/TableTools/swf/copy_csv_xls_pdf.swf')
.withTableToolsButtons([
'copy',
'print', {
'sExtends': 'collection',
'sButtonText': 'Save',
'aButtons': ['csv', 'xls', 'pdf']
}
]);
//adminService to request for all administrators
app.factory('adminService', ['ApiService', function (ApiService) {
return {
loadAdmin: function () {
ApiService.get("admin").then(function (response) {
if (response) {
if (response.success === true) {
return response;
}else{
console.log(response);
}
}else {
console.log('error request ');
}
});
}
};
}]);
//apiservice to interact with api
app.factory('ApiService', function ($http, $q, $localStorage) {
return {
get: function (apiresource) {
var returnData = $q.defer();
$http({
url: api + apiresource,
method: 'GET',
headers: {"Auth-Token": $localStorage.user_data.auth_token}
})
.success(function (data) {
returnData.resolve(data);
})
.error(function (error) {
returnData.resolve();
});
return returnData.promise;
}};
});`enter code here`
When ever I am in that view it throws this errorCannot read property 'then' of undefined. I am following examples from these two sources
http://www.revillweb.com/angularjs-by-example/4-sharing-data-with-angularjs-services/
http://l-lin.github.io/angular-datatables/#/withPromise
You need to return promise object (result of ApiService.get("admin") call) from loadAdmin method.
Also make sure you don't "swallow" rejections inside of the then (in console.log branches) - what happens when you unintentionally handle errors by not passing it further. For this return rejected promise or simply throw error, so that rejection will propagate further down the promise chain:
app.factory('adminService', ['ApiService', function (ApiService) {
return {
loadAdmin: function () {
return ApiService.get("admin").then(function (response) {
if (response) {
if (response.success === true) {
return response;
} else{
console.log(response);
throw response;
// or custom error object: throw {message: 'Error loadAdmin', response}
}
} else {
console.log('error request ');
throw new Error('error request');
}
});
}
};
}]);

Angular $q catch block resolves promis?

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);
});
});
};

$q promise error callback chains

In the following code snippet error 1 and success 2 will be logged. How can I can I propagate error callbacks being invoked rather than the success callbacks being invoked if the original deferred is rejected.
angular.module("Foo", []);
angular
.module("Foo")
.controller("Bar", function ($q) {
var deferred = $q.defer();
deferred.reject();
deferred.promise
.then(
/*success*/function () { console.log("success 1"); },
/*error*/function () { console.log("error 1"); })
.then(
/*success*/function () { console.log("success 2"); },
/*error*/function () { console.log("error 2"); });
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="Foo">
<div ng-controller="Bar"></div>
</div>
Error is propagate by returning $q.reject in the error callback
var deferred = $q.defer();
deferred.reject();
deferred.promise
.then(
/*success*/function () { console.log("success 1"); },
/*error*/function () { console.log("error 1"); return $q.reject('error 1')})
.then(
/*success*/function () { console.log("success 2"); },
/*error*/function () { console.log("error 2"); });
});
think of success/failure as try/catch
try{
var val = dummyPromise();
} catch (e){
val = "SomeValue";
}
if catch does not throws an exception, it is considered that the error is handled and hence outer calling function does not sees the error which occured in inner function.
Similar stuff happening here, you have to return return $q.reject(); from a promise in order for the next promise in the chain to fail too. See example plunker: http://plnkr.co/edit/porOG8qVg2GkeddzVHu3?p=preview
The reason is: Your error handler may take action to correct the error. In your error-function your dealing with the error,if not specified otherwise, it will return a new promise which is resolved. Therefore it is not reasonable to have the next promise failing by default (try-catch analogy).
By the way, you can return $q.reject() even from a success handler, if you sense an error condition, to have the next promise in the chain failing.
You're catching the error and handling it - so it gets to the success handler. If you want to reject it, you have to do it by returning $q.reject();
To sum the comments up, to propagate errors in the promise chain, either:
1) Do not provide an errorCallback for then:
deferred.promise
.then(
/*success*/function () { console.log("success 1"); },
.then(
/*success*/function () { console.log("success 2"); },
/*error*/function () { console.log("error 2"); }); // gets called
Or
2) Return $q.reject() from the errorCallback:
deferred.promise
.then(
/*success*/function () { console.log("success 1"); },
/*error*/function (err) { console.log("error 1"); return $q.reject(err); });
.then(
/*success*/function () { console.log("success 2"); },
/*error*/function () { console.log("error 2"); }); // gets called
From the angular $q.reject documentation:
This api should be used to forward rejection in a chain of promises.

How do I test an $httpBackend POST error state with AngularJS / Karma?

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);
});

Resources