I have a service with a method that gets me a list of project types using a $resource. It's working well for me, except that if I make multiple nearly simultaneous calls (from say, two directives) each will create another request instead of using the same response/$promise/data.
I found this which led me to this and TL;DR, apparently it's creating a redundant $q.defer() and is actually considered to be a deferred anti-pattern.
The code below works well if the calls to get project types are significantly staggered (like more than milliseconds apart). The consecutive calls are resolved with the shared.projectTypes. It also works in the sense that if the request to get project types fails, the dfr.reject()
will be triggered and be caught by .catch
in the calling controller.
angular.module('projects')
.factory('projectService', function(notificationService){
// an object to share data gathered by this service
var shared = {};
// $resource for projects API
var projectResource = $resource(baseApiPath + 'projects', {}, {
...,
getProjectTypes: {
method: 'GET',
url: baseApiPath + 'projects/types'
},
...
});
// loads a list of project types
var loadProjectTypes = function(){
var dfr = $q.defer();
// if we've already done this, just return what we have.
if(shared.projectTypes){
dfr.resolve(shared.projectTypes);
}
else {
// begin anti-pattern (?)
projectResource.getProjectTypes(null,
function(response){
shared.projectTypes = response.result.projectTypes;
dfr.resolve(response);
},
function(errResponse){
console.error(errResponse);
notificationService.setNotification('error', errResponse.data.messages[0]);
dfr.reject(errResponse);
});
}
return dfr.promise;
};
return {
shared: shared,
project: projectResource,
loadProjectTypes: loadProjectTypes
};
});
So, I read that having this extra var dfr = $q.defer()
is not necessary as the $resource would provide all that for me. With a bit of refactoring, I ended up with this:
...
// $resource for projects API
var projectResource = $resource(baseApiPath + 'projects', {}, {
...,
getProjectTypes: {
method: 'GET',
url: baseApiPath + 'projects/types',
isArray: true,
transformResponse: function(response){
return JSON.parse(response).result.projectTypes;
}
},
...
});
// loads a list of project types
var loadProjectTypes = function(){
return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes());
};
...
To clarify, I have added isArray
and transformResponse
to the resource because my API returns a lot of extra meta information and all I wanted was an array of types. In my loadProjectTypes
method, I'm including the same caching we originally had, but I'm caching the result of projectResource.getProjectTypes()
instead of the actual response data (even though that might be exactly what I'm caching because of the transformResponse).
This works on the happy path (reduced calls to API, returns the same thing to everyone, etc) but my main problem is with the chaining and catching of errors.
In my original anti-pattern example, if there is an error with GET /project/types, I'm using dfr.reject()
which is then passed back to my controller where I have a .catch().
This is code from the controller which actually makes the original request to get project types:
$q.all([
projectService.loadProjects(),
userService.loadUserRole('project_manager'),
userService.loadUserRole('sales_representative'),
projectService.loadProjectTypes(),
clientService.loadClients()
])
.then(function(response){
// doing stuff with response
})
.catch(function(errResponse){
// expecting errors from service to bubble through here
console.error(errResponse);
});
With the anti-pattern example, the dfr.reject
is causing the error to show up here in the catch, but in my supposed non-anti-pattern example, it's not happening. I'm not sure how to reject or resolve the $resource results in the same way I was before. If one of the points of promise chaining is to have one spot to handle errors from any chain link, I was doing it right.
I tried to use $q.resolve()/reject(), since I don't have dfr anymore, but this seems dumb and doesn't work anyway.
return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes(null,
function(response){
return $q.resolve(response);
},
function(errResponse){
return $q.reject(errResponse);
}));
How do I get the chain to work so that .catch() in the controller is where the errors get handled?
Did I actually implement the anti-pattern in my original code, or was that one of the accepted ways to use $q.defer() and it wasn't an anti-pattern at all?
In the second link I posted, there is an answer that says:
"What's wrong with it? But the pattern works! Lucky you. Unfortunately, it probably doesn't, as you likely forgot some edge case. In more than half of the occurrences I've seen, the author has forgotten to take care of the error handler."
However, my original code was addressing the errors. It was working, except that each caller was getting it's own promise. I feel that's where I missed something.
I might be confused, but I'm thinking that the loadProjectTypes
method should return the same promise/data to anyone who calls it, no matter when it's called. It should be the one true source of anything projectTypes and only make the call once, the very first time.
Any time I look for any of this (lots of purple/visited google links on these subjects), everyone is either showing chaining with contrived examples, or only using $http, or something else. I haven't found anyone doing error catching in a promise chain that uses $resource.
UPDATE: Adding my requirements for the solution. I posted them in my answer, but wanted to include them in the original post too.
Requirement 1: Allows multiple calls to the method, but only makes one API request which updates all callers with the same data.
Requirement 2: Must be able to use result of method as actual data, just as the promise spec intends. var myStuff = service.loadStuff()
should actually set myStuff
to be "stuff".
Requirement 3: Must allow promise chaining so that all errors in any part of the chain can be caught by a single catch at the end of the chain. As I've found in my solution, there can be more than one chain, and more than one catch, but the point is that each chain has a catch, and any "links" in the chain that break should all report their errors to their respective catch.