I have a web page where there are a bunch of items that the user can click on. clicking on any item, depending on it's type, will send an ajax request to the server and then display more items.
If the request results in an error, I want to display it and then allow the user to continue clicking or interacting with the page as before.
My code looks like this
$scope.$createObservableFunction("clickHandler")
.flatMapLatest(function (args) {
//send the ajax request to the server
})
.retry()
.subscribe(function (data) {
//handle getting the data from the server
})
where exactly can I handle the error case? I expect errors to happen, and I always want to re-subscribe to the source, but I want a chance to handle that error.
The trick is to turn your errors into data:
$scope.$createObservableFunction("clickHandler")
.flatMapLatest(function (args) {
//send the ajax request to the server
var ajaxQuery = someObservable;
// turn the observable into
// a stream of eithers, which will
// either have a 'result'
// or an 'error'
// use .catch() to turn the error
// into a either with an error property
// use .map() to turn the success
// into a either with the result property
return ajaxQuery
.map(function (result) {
return { result: result };
})
.catch(function (error) {
return Rx.Observable.of({ error: error });
});
})
.subscribe(function (data) {
if (data.error) {
// display data.error to user
}
else {
// display data.result to user
}
})
If your ajax method returns a Promise, use then chaining:
$scope.$createObservableFunction("clickHandler")
.flatMapLatest(function (args) {
//send the ajax request to the server
var ajaxQuery = somePromise;
// turn the promise into
// a promise of eithers, which will
// either have a 'result'
// or an 'error'
return ajaxQuery
.then(function onSuccess(result) {
return { result: result };
}, function onError (error) {
return { error: error };
});
})
.subscribe(function (data) {
if (data.error) {
// display data.error to user
}
else {
// display data.result to user
}
})
Related
I have a method in my dojo class which makes a request (say, a JSON one). If it succeeds, well and good. However, if it fails (times out or whatever), I want it to try again until it succeeds. To do this, I call the method itself in the error callback:
doReq: function(){
var req = Request(...);
return req.then(function(response, io){
// Success!
}, dojo.hitch(this, function(error, io){
this.doReq(); // Failed; try again.
}));
}
Am I doing this correctly?
It can be done this way, but you may want to limit attempts,
for example:
doReq: function(attempts){
attempts -= 1;
var req = Request(...);
return req.then(function(response, io){
// Success!
}, dojo.hitch(this, function(error, io){
if (attempts > 0) this.doReq(attempts); // Failed; try again.
else //return some error here
}));
}
I'm not sure why you return req.then(...), this will return new promise not the req's promise.
But if you want the caller of doReq to get response when the req succeeds, you can do it like this.
_request: function (deferred) {
var req = Request(...);
req.then(dojo.hitch(this, function (response, io) {
// Success!
deferred.resolve(response);
}), dojo.hitch(this, function (error, io) {
this._request(deferred); // Failed; try again.
// use deferred.reject('some error message'); to reject when it reached the retry limit, if you want to.
}));
},
doReq: function () {
var deferred = new Deferred(); // from module "dojo/Deferred"
this._request(deferred);
return deferred.promise;
}
This is how to use it.
var thePromise = this.doReq();
thePromise.then(dojo.hitch(this, function (response) {
console.log('response: ', response); // your response from deferred.resolve(response);
}), dojo.hitch(this, function (error) {
console.log('error: ', error); // your error from deferred.reject('some error message'); if you have.
}));
What I am trying to get done is extend JSON object in service and then pass it to controller.
JSON came to service from another service which makes backend call.
The code is pretty complicated so I add comments and console.logs:
//get games config object from another service
gamesConfig: gamesConfigService.gamesConfig(),
// prepare name of games icons. This is support function executed in next method
transformSpace: function(subject) {
var ensuredSubject = subject.toString().toLowerCase();
var transformedSubject = ensuredSubject.replace(/ /g, '_');
return transformedSubject;
},
//add iconname property to game config object
extendGameConfig: function() {
var that = this;
this.gamesConfig
.then(function (response) {
console.log(response.data); // this works and console.log my JSON
response.data.map(function(obj) {
return new Promise(function(res){
angular.extend(obj, {
iconname: that.transformSpace(obj.attributes.name) + "_icon.png"
});
});
});
}, function () {
console.log('errror');
});
This contains one support method transformSpace and main method which is not passing data correctly. ( I think )
I'm trying to receive this promise in controller by:
theService.getGamesObj.extendGameConfig()
.then(function (response) {
$scope.allGames = response;
console.log($scope.allGames);
}, function () {
console.log('err')
});
And then I'll use it in view. For now code above doesn't work and give me following error:
TypeError: Cannot read property 'then' of undefined
I've added comments where I think your code has gone wrong
extendGameConfig: function() {
// ***********
// use => functions, that = this wont be needed
var that = this;
// ***********
// if you want this this function to return something, add a return
// this is why you get the
// Cannot read property 'then' of undefined error
// as this function returns undefined
this.gamesConfig
.then(function (response) {
console.log(response.data); // this works and console.log my JSON
// ***********
// you're using .map ... and discarding the result!
response.data.map(function(obj) {
// ***********
// you're creating a promise that never resolves!
// also, why are you promisifying synchronous code?
return new Promise(function(res){
angular.extend(obj, {
iconname: that.transformSpace(obj.attributes.name) + "_icon.png"
});
});
});
}, function () {
console.log('errror');
});
so, try this
extendGameConfig: function() {
return this.gamesConfig
.then(response => {
return response.data.map(obj => {
return angular.extend(obj, {iconname: this.transformSpace(obj.attributes.name) + "_icon.png"});
});
}, function () {
console.log('errror');
});
or, better yet
extendGameConfig: function() {
return this.gamesConfig
.then(response =>
response.data.map(obj =>
angular.extend(obj, {iconname: this.transformSpace(obj.attributes.name) + "_icon.png"})
)
)
.catch(function (err) {
console.log('error', err);
throw err; // log the error, but you'll probably want to reject this promise so the calling code doesn't think there is success?
});
}
Before making an HTTP request, I need to check if the access credentials I have are valid. If they are not valid, I need to make a first HTTP request to revalidate them and, after completion, then a second HTTP request (the original one). The function call needs to return Angular's $http promise from the second HTTP request. Here's my function:
var makeRequest = function (scope, address, request, basic, form) {
startLoad(scope);
// Check if user is logged in and needs a new session token...
if (ready() && (now() > getSessionExpires()-20) ) {
// Setup auth.session.refresh request
var refreshRequest = {
"refreshToken": getRefreshToken(),
"clientID": getClientID(),
};
// Make auth.session.refresh request
$http.post(API + 'auth.session.refresh', format(refreshRequest))
.error(function (data) {
// If refresh request fails, logout and redirect to expired message
stopLoad(scope); logoff();
$window.location.href = '/error/expired';
return;
})
.success(function (data) {
// If refresh request succeeds, makeRequest is called recursively and the else condition runs
process(data, true);
return makeRequest(scope, address, request, basic, form);
});
} else { // if not logged in, or if session token is valid, run request function as usual
// Add Authorization header with valid sessionToken
if (ready()) $http.defaults.headers.post['Authorization'] = 'Bearer ' + getSessionToken();
// Basic Request: returns promise (if next context not set, can chain other action)
if (basic) {
return $http.post(API + address, request)
.error(function(data) {
if (data && scope) stopLoad(scope, data.message);
else if (scope) stopLoad(scope, Defaults.serverError);
else stopLoad();
if (form) resetForm(form);
})
.success(function(data) {
process(data);
if (scope) {
stopLoad(scope);
if (scope.context.next) $location.path(scope.context.next);
} else stopLoad();
if (form) resetForm(form);
});
}
// Custom Request: returns promise (can chain .error, .success)
else return $http.post(API + address, request);
}
};
When the token is found to be invalid, however, the function returns undefined, and I get an error that I cannot run .success() or .error(). The else functionality runs, but I'm wondering how I can ensure that I don't get this error. Thank you!
Just return the upper $http.post(/*...*/) and let promise chaining do it's magic:
return $http.post(API + 'auth.session.refresh', format(refreshRequest))
.catch(function (response) {
// If refresh request fails, logout and redirect to expired message
stopLoad(scope); logoff();
$window.location.href = '/error/expired';
})
.then(function (response) {
// If refresh request succeeds, makeRequest is called recursively and the else condition runs
process(response.data, true);
return makeRequest(scope, address, request, basic, form);
});
UPDATE: since .success/.error functions are not chainable (and have been flagged deprecated), you should use .then and .catch instead.
$http.post(/*...*/)
.success(function(data) {
/* do something with data */
})
.error(function(err) {
/*...*/
});
becomes
$http.post(/*...*/)
.then(function(response) {
/*do something with response.data */
})
.catch(function(response) {
/*...*/
});
as I understand it Angular http has 2 checks 'success and 'error'. Thats in terms of connecting to the service or not - so I have that in hand and thats my first check.
The issue I have is that the data in my JSON has a success state which informs me if the data it contains or has received from my form had any problems with it, in which case there will be an error object that I act on and display to the user.
I need to check for that value of success, but where is the best place to check for that?
Should I be doing it in the controller?
Without that data being correct theres nothing else for the page to do so it is effectively the first thing that needs to be done after the data is retrieved.
heres the basic controller layout
app.controller("dataCtrl", function ($scope, $http) {
$http.post('/getdata').success(function (data) {
$scope.businessData = data;
// Should I then be checking businessData.success at this level?
}).error(function () {
alert("Problem");
});
});
You can write something like this:
$http.post('/getdata').success(function (data) {
if (validate(data)) {
$scope.businessData = data;
} else {
$scop.buisnessDataError = {msg: 'smth bad happend'};
}
}).error(function () {..})
Otherwise, you can write your validator in Promise-like style and then just chain promises in such manner:
$http.post('/getdata').then(function (res) {
return validator(null, res.data);
}, function (err) {
return validator({msg: 'error'})
}).then(function (data) {
//proceed your data
}, function (err) {
alert(err.msg);
});
Where validator is:
var varlidator = function (err, data) {
return $q(function (resolve, reject) {
if (/*data is not valid*/ || err) {
reject(err);
} else {
resolve(data);
}
});
}
$q is a standard angulars implementation of Promises
I have a function which creates a deferred object. On fail I'm calling a fallback function, which in turn creates and returns it's own deferred/promise object. I would like to return the result of this fallback-deferred but I'm only able to return the error on my initial call.
Here is what I'm doing:
// method call
init.fetchConfigurationFile(
storage,
"gadgets",
gadget.getAttribute("data-gadget-id"),
pointer
).then(function(fragment) {
console.log("gotcha");
console.log(fragment);
}).fail(function(error_should_be_fragment) {
console.log("gotcha not");
console.log(error_should_be_fragment);
});
My fetchConfiguration call tries to load from localstorage and falls back to loading from file if the document/attachment I need is not in localstorage.
init.fetchConfigurationFile = function (storage, file, attachment, run) {
return storage.getAttachment({"_id": file, "_attachment": attachment})
.then(function (response) {
return jIO.util.readBlobAsText(response.data);
})
.then(function (answer) {
return run(JSON.parse(answer.target.result))
})
.fail(function (error) {
// PROBLEM
console.log(error);
if (error.status === 404 && error.id === file) {
return init.getFromDisk(storage, file, attachment, run);
}
});
};
My problem is I can catch the 404 allright, but instead of returning the error object, I would like to return the promise generated by init.getFromDisk.
Question:
Is it possible to return the result of my getFromDisk call in the error handler? If not, how would I have to structure my calls so that I'm always returning a promise to my first method call?
Thanks for help!
SOLUTION:
Thanks for the help! Fixed it like this:
init.fetchConfigurationFile(
storage,
"gadgets",
gadget.getAttribute("data-gadget-id"),
pointer
).always(function(fragment) {
console.log("gotcha");
console.log(fragment);
});
init.fetchConfigurationFile = function (storage, file, attachment, run) {
return storage.getAttachment({"_id": file, "_attachment": attachment})
.then(function (response) {
return jIO.util.readBlobAsText(response.data);
})
.then(
function (answer) {
return run(JSON.parse(answer.target.result));
},
function (error) {
if (error.status === 404 && error.id === file) {
return init.getFromDisk(storage, file, attachment, run);
}
}
);
};
.fail() always returns the original promise.
You should call then() with a failure callback to allow chaining:
.then(undefined, function(error) {
return ...;
});
Before jQuery 1.8, use .pipe() instead.