I'm working on an Angular application which shows a list of items fetched from a RESTful API. The content of the list depends on a query.
The query can be passed either by filling in an input field, using a submit button or by adding it to the URL as query parameter.
To make sure everything runs in order and prevent asynchronous issues, I’m using RxJS.
Now I’m wondering how I can handle errors, since they can occur in the middle of a stream, eg. when the HTTP-request fails.
The query as input stream
These are two Observables, which both send a sequence of queries.
// first observable for the submit button
submitFormObservable = $scope.$createObservableFunction('search');
// second observable for when the input value changes
inputObservable = $scope.$toObservable('query')
.map(function (change) {
return change.newValue;
});
Fetching for the results
The Observable below triggers when the query has been changed and fetches the results from the API.
var mainStream = Rx.Observable.merge([submitFormObservable, inputObservable])
.where(function (query) {
return query && query.length > 0;
})
.debounce(400)
.distinctUntilChanged()
.select(getResultsForQuery)
.switchLatest();
Handling errors
Now I don’t know how to handle errors when eg. the getResultsForQuery throws an error.
I would like to display the error instead of the results, but prevent the Observable from handling new events.
Currently I've solved it by creating two new streams from the Observable, one to handle the result when successful and one when an error occurs. The response data for when the query was invalid contains an error property.
Success stream
// stream containing the album information from LastFm
mainStream
.filter(function (response) {
return !response.data.error;
})
.map(function (response) {
return response.data.result;
})
.subscribe(function (result) {
$scope.error = undefined;
$scope.result = result;
});
Error stream
mainStream
.filter(function (response) {
return response.data.error;
})
.map(function (response) {
return response.data;
});
.subscribe(function (error) {
$scope.result = [];
$scope.error = error;
});
Possible solutions
I've read about throwing and catching errors, but the problem here is the stream seems to stop after the first error and doesn't trigger new events.
Using onErrorResumeNext is described in the docs to ignore errors and make sure the stream continues after an error. But I can't find a way to correctly 'handle' the error and show it to the end user.
Question
Is there a recommended approach on how to handle errors in a situation like this? Is it necessary to create two streams for this, or is it recommended to throw an exception?
It's important that the user knows something went wrong, and the stream doesn't stop after the first error.
The issue with catching logic is that it effectively terminates the sequence before it, which is why if you use it in your top level stream it will stop after a single exception. I would recommend that you wrap your catch logic inside of a flatMapLatest. Then you can catch on the inner stream and transform the data to conform with what your downstream observers expect.
Something like this:
var mainStream = Rx.Observable.merge([submitFormObservable, inputObservable])
.where(function (query) {
return query && query.length > 0;
})
.debounce(400)
.distinctUntilChanged()
.selectSwitch(function(input) {
return getResultsForQuery(input)
.map(function(response) {
return {result : response.data.result};
})
.catch(function(e) {
return Rx.Observable.just({error : error});
});
});
mainStream.subscribe(function(r) {
$scope.result = r.result || [];
$scope.error = r.error;
});
Related
I built a push notification system on my backend using expo-server-sdk-node. When I want to send notifications, I lookup the expoPushToken in my database. The docs states the following error(s) should be handled:
DeviceNotRegistered: the device cannot receive push notifications
anymore and you should stop sending messages to the corresponding Expo
push token.
However, I am unsure how to handle this error since there are no direct pushTokens available in the error message. See the following example:
[{
status: 'error',
message: '"ExponentPushToken[XXXXXXXXXXXXXXX]" is not a registered push notification recipient',
details: { error: 'DeviceNotRegistered' }
}]
This device should now be removed from my database, but to do that I need the ExponentPushToken[XXXXXXXXXXXXXXX] value. And because the notifications are sent in batches I lose the reference to the user. What is the proper way to do this?
I thought of the following two ways:
1: Just split(") and filter the value, but this depends on the error message.
2: Loop over all my pushTokens, and find where includes(originalValue) in message, but this would mean I'd have to loop over an excessive amount of users every time it fails.
Any recommendations?
I faced the same issue, and here's what I did.
Considering this code
for (let chunk of chunks) {
try {
let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
console.log(ticketChunk);
tickets.push(...ticketChunk);
// If a ticket contains an error code in ticket.details.error
//
} catch (error) {
console.error(error);
}
}
Once I send a batch of notifications (100 most likely).
I loop through the tickets, if ticket.status === 'error' and check for ticket.details.error === 'DeviceNotRegistered' as in the code above.
Since the order of sent notifications is the order in which the response tickets are received.
Using the current index of the tickets loop, I can access the token at the same index in the chunk I sent.
for (let chunk of chunks) {
try {
let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
tickets.push(...ticketChunk);
// If a ticket contains an error code in ticket.details.error
let ticketIndex = 0;
for (let ticket of tickets) {
if (ticket.status === 'error' && ticket.details.error === 'DeviceNotRegistered') {
// Get the expo token from the `chunk` using `ticketIndex`
// Unsubscribe the token or do whatever you want to
}
ticketIndex++;
}
} catch (error) {
console.error(error);
}
}
NB: The code might contain syntax errors, it's the idea I am trying to pass across. I did the same thing with php
It's not documented behaviour (at least I didn't found it in the documentation), but in the ticket error response I can see the expoPushToken in the details object. See the screenshot attached:
I am using angularjs for a web site where I have to download data every seccond. To do that I am using a WebSocket, the problem is that I am sending a lot of different requests of different types. To elaborate the request I use Promises but some times the answers get to the wrong function. Let me make an example:
So I have a service that is making the request and gets the result as Promise.
socketService.$inject = [];
function socketService(){
let service = this;
let server = new WebSocket('ws://localhost:8090');
let isOpen = false;
server.onopen = function(){
isOpen = true;
};
server.onerror = function(){
//TODO handle error
};
service.sendRequest = function(action, data){
return new Promise(function (resolve, reject) {
if (isOpen) {
server.send(encodeRequest(action, data));
server.onmessage = function (message) {
resolve(JSON.parse(message.data));
}
}
server.onopen = function () {
server.send(encodeRequest(action, data));
server.onmessage = function(message) {
resolve(JSON.parse(message.data));
isOpen = true;
}
};
server.onerror = function (error) {
reject(error);
}
})
};
}
And I use it like this:
socketService.sendRequest('get_events', {})
.then(function (response) {
//DO SOMETHING WITH THE RESPONSE
return socketService.sendRequest('get_history', {
fromDate: value,
toDate : value,
tag : value,
event : value
})
})
.then(function (response) {
//DO SOMETHING WITH THE RESPONSE
});
The problem is that if I make the requests like this (when one promise is resolved I call the other it works perfectly) but if instead I do for example some requests every second and then in the same time I make a new request (socketService.sendRequest(...)) the results get messed up.
I figured out that the problem is that if I make a request and before I get the result of the first request I make another one, the response of the first request goes to the second one.
I don't understand why this happens because I return every time a new Promise, so it should resolve the response of that Promise instead of resolving the response of the next Promise.
I read that once a promise is resolved it cannot be used again (it returns the last result over and over) but I create a new one every time.
Can someone explain to me what happens and how I could fix this?
You'll need to put a unique ID into the messages so that you can ensure that each response gets correlated to the correct request. But before diving into coding this, you may want to look at json rpc 2.0. This is a standard, with a couple of different javascript implementations, that will handle most of this for you. I have had good luck with https://github.com/jershell/simple-jsonrpc-js
You also asked why your Promise code is not working the way you expect it to. The answer is... well, Promise doesn't work the way you expect it to. :-) OK, let me try to be more helpful. Your Promise code is altering the server's callback methods (onOpen, onMessage, etc.) so that they will call the promise's "resolve" function. The problem is that, even though you have multiple promises, you only have one server. So each Promise you create just points the server callback methods to a new resolve function.
But... skip all that. Someone else has already figured this out for you. Use JSON RPC 2.0.
Duncan
I was also facing similar issues, as WebSocket message work as events. I have written one library where we can write Promise based send and receive the message.
https://github.com/pathikdevani/linka
I'm trying to understand how to consume observable sequences and how to recover from errors.
My example is a bit contrived but my real implementation is a bit too complex to show here. Anyway, I have someObservable that emits values when the user clicks in the UI. This should trigger a request to the API (GET/POST/etc). The problem is that if the API returns an error, postData isn't called anymore after that.
I've make an example here that shows the problem. I've found that I can use Rx.Observable.of instead of Rx.Observable.throw to keep things running. But that will emit a value to the subscribe function. And in my real application postData is a service that is reused by multiple parts of the application and I'm not sure that I want to use of instead of throw there.
So my question is, how do you normally handle things like these?
let someObservable = Rx.Observable.timer(1, 1000);
someObservable
.switchMap(data => {
return postData(data);
})
.catch(error => {
console.log("POST failed");
})
.subscribe(val => console.log("Success: " + val));
function postData(data) {
console.log("Will post " + data);
if (data !== 2) {
return Rx.Observable.of(true);
}
else {
return Rx.Observable.throw(new Error("400 Bad Request or something"));
}
}
http://jsbin.com/naroyeyive/3/edit?js,console
If you want to send the request again, you may want to look into retry and retryWhen instead of catch.
catch will use the returned Observable as new "source". See this bin for an example.
retry(When) will "re-subscribe" whenever an error occurs, based on the configuration you pass to the operators. You can find examples and more information here: https://www.learnrxjs.io/operators/error_handling/retrywhen.html
Im currently implementing a Purchase Order type View. Where I have a PurchaseOrder table and a PurchaseOrderLine table for the items. The first thing I do when the use presses the save button I first save the Purchase Order and then I retrieve the PurchaseOrderID and save to each individual PurchaseOrder item. The problems is the following:
Promise.resolve( app.PurchaseOrder.create(formData) ).then(function(response){
purchaseOrderID = response.collection.models[0].attributes.id;
for(var key in formLineData){
if(formLineData.hasOwnProperty(key)){
formLineData[key]['requestID'] = purchaseOrderID;
app.PurchaseOrderLines.create(formLineData[key]);
}
}
}).catch(function(error){
console.log(error);
})
formData is the PurchaseOrder data, formLineData is the PurchaseOrderLine Data(I do the for loop to insert requestIDs to all items).
I am using a Promise because collection.fetch does not return a promise on Backbone(I think my implementation isn't quite correct because Promise.resolve() is use to make thenables a promise and in this case it isn't). The problem is that when the save button is clicked the then part passes even PurchaseOrder hasn't been created. So when it gets to the PurchaseOrderLine.create, all the items are saved without a PurchaseOrderID. I have two options:
Add a server validation for this. The problem with this is that everytime is going to return an error and this can be bothersome to the user.
Add a setTimeout to at least wait a couple seconds for the write to over on the server.
Could please shed a light on this topic.
you can try something like this
app.PurchaseOrder.create(formData).then(function (response) {
var purchaseOrderID = response.collection.models[0].attributes.id;
return new Promise(async function (resolve) {
for (var key in formLineData) {
if (formLineData.hasOwnProperty(key)) {
formLineData[key]["requestID"] = purchaseOrderID;
await app.PurchaseOrderLines.create(formLineData[key]);
}
}
resolve()
});
});
or maybe doing something like this using Promise.all
Promise.all(formLineData.map(()=>app.PurchaseOrderLines.create(formLineData[key])))
Here's the relevant code:
var Results = mongoose.model('Results', resultsSchema);
var results_array = [];
_.each(matches, function(match) {
var results = new Results({
id: match.match_id,
... // more attributes
});
results_array.push(results);
});
callback(results_array);
});
}
], function(results_array) {
results_array.insert(function(err) {
// error handling
Naturally, I get a No method found for the results_array. However I'm not sure what else to call the method on.
In other functions I'm passing through the equivalent of the results variable here, which is a mongoose object and has the insert method available.
How can I insert an array of documents here?
** Edit **
function(results_array) {
async.eachLimit(results_array, 20, function(result, callback) {
result.save(function(err) {
callback(err);
});
}, function(err) {
if (err) {
if (err.code == 11000) {
return res.status(409);
}
return next(err);
}
res.status(200).end();
});
});
So what's happening:
When I clear the collection, this works fine.
However when I resend this request I never get a response.
This is happening because I have my schema to not allow duplicates that are coming in from the JSON response. So when I resend the request, it gets the same data as the first request, and thus responds with an error. This is what I believe status code 409 deals with.
Is there a typo somewhere in my implementation?
Edit 2
Error code coming out:
{ [MongoError: insertDocument :: caused by :: 11000 E11000 duplicate key error index:
test.results.$_id_ dup key: { : 1931559 }]
name: 'MongoError',
code: 11000,
err: 'insertDocument :: caused by :: 11000 E11000 duplicate key error index:
test.results.$_id_ dup key: { : 1931559 }' }
So this is as expected.
Mongo is responding with a 11000 error, complaining that this is a duplicate key.
Edit 3
if (err.code == 11000) {
return res.status(409).end();
}
This seems to have fixed the problem. Is this a band-aid fix though?
You seem to be trying to insert various documents at once here. So you actually have a few options.
Firstly, there is no .insert() method in mongoose as this is replaced with other wrappers such as .save() and .create(). The most basic process here is to just call "save" on each document you have just created. Also employing the async library here to implement some flow control so everything just doesn't queue up:
async.eachLimit(results_array,20,function(result,callback) {
result.save(function(err) {
callback(err)
});
},function(err) {
// process when complete or on error
});
Another thing here is that .create() can just take a list of objects as it's arguments and simply inserts each one as the document is created:
Results.create(results_array,function(err) {
});
That would actually be with "raw" objects though as they are essentially all cast as a mongooose document first. You can ask for the documents back as additional arguments in the callback signature, but constructing that is likely overkill.
Either way those shake, the "async" form will process those in parallel and the "create" form will be in sequence, but they are both effectively issuing one "insert" to the database for each document that is created.
For true Bulk functionality you presently need to address the underlying driver methods, and the best place is with the Bulk Operations API:
mongoose.connection.on("open",function(err,conn) {
var bulk = Results.collection.initializeUnorderedBulkOp();
var count = 0;
async.eachSeries(results_array,function(result,callback) {
bulk.insert(result);
count++;
if ( count % 1000 == 0 ) {
bulk.execute(function(err,response) {
// maybe check response
bulk = Results.collection.initializeUnorderedBulkOp();
callback(err);
});
} else {
callback();
}
},function(err) {
// called when done
// Check if there are still writes queued
if ( count % 1000 != 0 )
bulk.execute(function(err,response) {
// maybe check response
});
});
});
Again the array here is raw objects rather than those cast as a mongoose document. There is no validation or other mongoose schema logic implemented here as this is just a basic driver method and does not know about such things.
While the array is processed in series, the above shows that a write operation will only actually be sent to the server once every 1000 entries processed or when the end is reached. So this truly does send everything to the server at once.
Unordered operations means that the err would normally not be set but rather the "response" document would contain any errors that might have occurred. If you want this to fail on the first error then it would be .initializeOrderedBulkOp() instead.
The care to take here is that you must be sure a connection is open before accessing these methods in this way. Mongoose looks after the connection with it's own methods so where a method such as .save() is reached in your code before the actual connection is made to the database it is "queued" in a sense awaiting this event.
So either make sure that some other "mongoose" operation has completed first or otherwise ensure that your application logic works within such a case where the connection is sure to be made. Simulated in this example by placing within the "connection open" event.
It depends on what you really want to do. Each case has it's uses, with of course the last being the fastest possible way to do this as there are limited "write" and "return result" conversations going back and forth with the server.