I have the following code. It works fine when f2 throws no error.
If there is an error, it generates an Unhandled rejection Error.
What's the proper way to rewrite the code to avoid Unhandled rejection Error and propagate it correctly to catch in f1?
let Bluebird = require('bluebird'),
mkdirp = Bluebird.promisify(require('mkdirp')),
request = Bluebird.promisify(require('request')),
writeFile = Bluebird.promisify(require('fs').writeFile);
function f1() {
.........
f2(path, fileName, options).then(.....).catch(....);
}
function f2(path, fileName, options) {
p = mkdirp(path).then(request(options).then(res => {
if (res[0].statusCode === 200) {
writeFile(fileName, res[0].body);
return res[0].body;
} else {
throw new Error(res[0].statusCode + ': ' + res[0].body);
}
}));
return p;
}
The problem is that you are passing a promise into .then() in f2. .then() will ignore anything that is not a function, so all that f2 is really returning is a promise for mkdirp(this.path) and that's a big bug for a few reasons. If an error is thrown in request(options)'s then handler, then there will be nothing to handle it.
Also, you are not doing anything to handle a possible error from writeFile. If you call writeFile, you either need to return a promise chain that includes it, or add logic to handle it within f2.
Since it looks like you can run mkdirp() and request() in parallel here, but you are not using the result of mkdirp() I would say this is the way to go:
function f2(path, fileName, options) {
var p = mkdirp(path).return(request(options)).then(res => {
if (res[0].statusCode === 200) {
return writeFile(fileName, res[0].body)
.return(res[0].body);
} else {
throw new Error(res[0].statusCode + ': ' + res[0].body);
}
});
return p;
}
Related
I found the following code in a project, that I do not understand.:
get(key, store = null) {
if (!key) {
return new Error('There is no key to get!');
}
let dbstore = this.localforage;
if (store !== null) {
dbstore = store;
}
return dbstore
.getItem(key)
.then(function(value) {
return value;
})
.catch(function(err) {
return new Error('The key (' + key + ") isn't accessible: " + err);
});
}
Why return new Error('There is no key to get!'); instead of throw new Error('There is no key to get!');?
Also why not throw an error in the catch block?
When you design a function interface and there are errors to deal with, you have a design choice for how to return errors. If the function is synchronous, you can either return some sentinel value that indicates an error and is easily distinguished from an actual result (often null in Javascript) or you can throw an exception or you can return an object that has a property that indicates the success or failure of the operation.
When you have an asynchronous operation with a promise interface, one would usually reject the Promise with an Error object as the reject reason to signify an error. That's the core design theory of promises. Success resolves with an optional value, errors reject with a reason.
This block of code:
return dbstore
.getItem(key)
.then(function(value) {
return value;
})
.catch(function(err) {
return new Error('The key (' + key + ") isn't accessible: " + err);
});
is resolving the returned promise with either a value or an Error object. This is generally not how promise code is written because it will require the caller to test the type of the resolved value to figure out if there's an error or not which is not the simple, straightforward way to use promises. So, to your question, you would usually do this:
return dbstore.getItem(key).catch(function(err) {
throw new Error('The key (' + key + ") isn't accessible: " + err);
});
There are other signs in this function, that it's just bad code.
.then(function(value) {return value;}) is completely superfluous and unnecessary. It adds no value at all. The value is already the resolved value of the promise. No need to declare it again.
The function sometimes returns a promise and sometimes throws a synchronous exception.
This is even a further pain to use. If you look at the first if (!key) { statement, it returns an Error object if the key argument isn't supplied. That means that to use this function you have to catch synchronous exceptions, provide .then() and .catch() handlers AND check the type of the resolved promise to see if it happens to be an error object. This function is a nightmare to use. It's bad code.
To use the function as it is, the caller would likely have to do this:
let retVal = someObj.get(aKey);
if (typeof retVal === Error) {
// got some synchronous error
} else {
retVal.then(val => {
if (typeof val === Error) {
// got some asynchronous error
} else {
// got an actual successful value here
}
}).catch(err => {
// got some asynchronous error
})
}
The function implementation probably should be this:
get(key, store = null) {
if (!key) {
return Promise.reject(new Error('There is no key to get!'));
}
let dbstore = store || this.localforage;
return dbstore.getItem(key).catch(function(err) {
throw new Error('The key (' + key + ") isn't accessible: " + err);
});
}
This can then be used like this:
someObj.get(aKey).then(val => {
// got some successful value here
}).catch(err => {
// got some error here
});
Compare the simplicity for the caller here to the mess above.
This implementation has these consistencies:
It always returns a promise. If key isn't supplied, it returns a rejected promise.
All errors come via a rejected promise.
The value the promise resolves with is always an actual successful value.
There's no .then() handler that does nothing useful.
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;
The code is a part of a much bigger and complicated code, so I am just gonna put the relevant snippets to my question.
I have this promise.all snippet:
Promise.all(receivedObjs.arrayCMsIds.map(cmid =>
server.writeAttachedCMinfo(invId,cmid)))
.then(function (results) { // for promise all
return res.json(apiHelp.success(results,"success"));
}).catch(function (error) {
res.json(apiHelp.error(error, error));
});
And this long complicated writeAttachedCMinfo function:
server.writeAttachedCMinfo = function (invId,cmid) {
return new Promise(function (resolve, reject) {
console.log("writeAttachedCMinfo");
console.log("invoiceId " + invId);
console.log("cmid "+ cmid);
var invoiceId = JSON.stringify(invId);
var cmId = JSON.stringify(cmid);
var invIdString = invoiceId;
var cmIdString = cmId;
invIdString = invIdString.slice(1, -1);
cmIdString = cmIdString.slice(1, -1);
var projection = 'gwCode certifiedInvoiceAmount buyerReference supplierReference invoiceNo invoiceSerialNo invoiceFiles creditMemos';
ubiqInvoice.findById(invIdString, projection).then(function (dbInvoice) {
var intInvCertifiedAmount = parseInt(dbInvoice.certifiedInvoiceAmount);
creditMemo.findById(cmIdString).then(function (dbCreditMemo) {
var intCreditMemoAmount = parseInt(dbCreditMemo.creditMemoAmount);
if (intInvCertifiedAmount <= intCreditMemoAmount) {
console.log('cm bigger than invoice')
return new Error ('CMisbiggerThanInvoice');
}
if (dbCreditMemo.isAssociated) {
return new Error ('CMisAssociated');
}
if (dbInvoice.gwCode === "100000000000"
|| dbInvoice.gwCode === "110000000000"
|| dbInvoice.gwCode === "111200000000"
|| dbInvoice.gwCode === "111100000000"
|| dbInvoice.gwCode === "111110000000"
) {
var creditMemoEntry = {
id: guid.create().value,
batchId: dbCreditMemo.batchId,
invoiceId: dbInvoice._id,
recordTypeCode: "CM",
buyerReference: dbInvoice.buyerReference,
supplierReference: dbInvoice.supplierReference,
creditMemoNo: dbCreditMemo.creditMemoNo,
creditMemoIssuingDate: dbCreditMemo.creditMemoIssuingDate,
creditMemoEffectiveDate: dbCreditMemo.creditMemoEffectiveDate,
lastModificationDate: dbCreditMemo.lastModificationDate,
currencyCode: dbCreditMemo.currencyCode,
creditMemoAmount: dbCreditMemo.creditMemoAmount,
hashCode: dbCreditMemo.hashCode,
description: dbCreditMemo.description,
uploadDate: dbCreditMemo.uploadDate,
isAssociated: true,
}
dbInvoice.creditMemos.push(creditMemoEntry);
dbInvoice.certifiedInvoiceAmount = dbInvoice.certifiedInvoiceAmount - dbCreditMemo.creditMemoAmount;
dbInvoice.save();
dbCreditMemo.isAssociated = true;
dbCreditMemo.save();
resolve(dbInvoice)
}
else { return new Error ('wrongggwcode'); }
})
});
}), function (error) {
console.log("error: " + error);
}
}
My goal, is to force error throwing in case one of the if conditions aren't met, and I want to pass the error to client in a form of a custom message so i can use it on the client said for displaying various errors , such as 'CMisbiggerThanInvoice'
if (intInvCertifiedAmount <= intCreditMemoAmount) {
console.log('cm bigger than invoice')
return new Error ('CMisbiggerThanInvoice');
}
I am just trying to figure out a way to pass the error from the writeAttachedCMinfo function to the promise.all's .catch(function (error) but it's not working, the promise.all is always returning success even if one of the if conditions aren't met.
I have tried reject('CMisbiggerThanInvoice'), reject(new Error('CMisbiggerThanInvoice')...all the same.
how can i really force the promise function to return an error?
In the context of a promise you should actually throw the error:
throw new Error('wrongggwcode');
If this executes in a promise constructor callback or then callback, it can be caught via the catch method (or second argument of then) and the argument of the callback you pass to it will be the error (object).
Calling reject from within a then callback will obviously not work, since you don't have access to reject there, but it will work in a promise constructor callback.
Simple example:
new Promise((resolve, reject) => {
setTimeout( () => reject(new Error('this is an error')) );
}).then( (value) => console.log('resolved with ' + value) )
.catch( (error) => console.log('error message: ', error.message) );
Nesting
When you have nested promises within then callbacks, make sure you always return the value returned by the inner promise as the return value of the outer then callback.
So in your case do this:
return creditMemo.findById( ....
//^^^^
For the same reason you need to do:
return ubiqInvoice.findById( ....
//^^^^
It would lead to far for this question/answer, but it is best practice to avoid nesting promise then calls all together. Instead of calling then on a nested promise, just return the promise without the then call, and apply that then call one level higher, so that you have a "flat" chain of then calls. This is just a matter of best practice, although it should also work like you have done it provided you always return the inner promises.
Position of the error handler
The error handler is placed at a wrong position; in fact you are using the comma operator. In short, you have this in your code:
new Promise(function (resolve, reject) {
// ... //
}), function (error) {
console.log("error: " + error);
}
The function after the comma is never executed as it is not an argument to a method call. It just sits behind a comma operator.
What you want is a catch method call on the new promise, and cascade the error in there so that Promise.all will also receive the rejection:
return new Promise(function (resolve, reject) {
// ... //
}).catch(function (error) {
console.log("error: " + error);
throw error; // cascade it
});
In server.writeAttachedCMinfo() the main things are :
to get rid of the new Promise() wrapper - it's not necessary - (Promise constructor anti-pattern).
to throw errors, rather than return them.
Also, because there's only one invId, there's effectively only one dbInvoice and it needn't be retrieved over and over from the database on each call of server.writeAttachedCMinfo(). In fact it shouldn't be retrieved over and over, otherwise each dbInvoice.save() may well overwrite earlier saves within the same overall transaction. It will be safer to accumulate all creditMemos and progressively decrement the certifiedInvoiceAmount in a single dbInvoice object, and evetually perform a single `dbInvoice.save().
server.writeAttachedCMinfo = function(dbInvoice, cmid) {
return creditMemo.findById(JSON.stringify(cmid).slice(1, -1))
.then(dbCreditMemo => {
if(parseInt(dbInvoice.certifiedInvoiceAmount) <= parseInt(dbCreditMemo.creditMemoAmount)) {
throw new Error('CMisbiggerThanInvoice');
// ^^^^^
}
/* all sorts of synchronous stuff */
/* all sorts of synchronous stuff */
/* all sorts of synchronous stuff */
return dbCreditMemo; // deliver dbCreditMemo via returned Promise
});
}
Now, in the caller :
ubiqInvoice.findById() can be called once.
perform checks on dbInvoice, and throw on failure.
pass dbInvoice, rather than invId to server.writeAttachedCMinfo() at each call.
all saving can be done here, not in server.writeAttachedCMinfo().
As a consequence, the caller subsumes some of the code that was in server.writeAttachedCMinfo() :
ubiqInvoice.findById(JSON.stringify(invId).slice(1, -1), 'gwCode certifiedInvoiceAmount buyerReference supplierReference invoiceNo invoiceSerialNo invoiceFiles creditMemos')
.then(dbInvoice => {
if(dbCreditMemo.isAssociated) {
throw new Error('CMisAssociated');
// ^^^^^
}
if(dbInvoice.gwCode === '100000000000'
|| dbInvoice.gwCode === '110000000000'
|| dbInvoice.gwCode === '111200000000'
|| dbInvoice.gwCode === '111100000000'
|| dbInvoice.gwCode === '111110000000'
) {
return Promise.all(receivedObjs.arrayCMsIds.map(cmid => {
return server.writeAttachedCMinfo(dbInvoice, cmid)
.catch(error => {
console.log(error);
return null;
});
}))
.then(dbCreditMemos => {
return Promise.all(dbCreditMemos.map(memo => {
return memo ? memo.save() : null;
}))
.then(() => dbInvoice.save())
.then(() => {
res.json(apiHelp.success(dbInvoice, 'success'));
});
});
} else {
throw new Error('wrongggwcode');
// ^^^^^
}
})
.catch(error => {
console.log('error: ' + error);
res.json(apiHelp.error(error, error));
});
The whole area around catching/handling errors arising from server.writeAttachedCMinfo() requires more thought. On the one hand, you might want successful sub-transactions to be saved and errors to be swallowed (as coded above) while on the other hand, you might want any single failure to cause nothing to be saved.
Another consideration is whether server.writeAttachedCMinfo() calls should be made sequentially, which would control the priority by which the sub-transactions access the credit balance. As it stands, with parallel requests, it's a bit of a free-for-all.
That addresses a bit more than the question asked for but hopefully it will be useful.
I am using requestify to make some HTTP requests. The module internally makes use other modules such as HTTP, socket etc. It also makes use of Q promises.
The modules used by requestify can asynchronously throw exceptions (e.g. ECONNREFUSED can be thrown by the socket module). Whenever an exception is thrown, the control flow breaks away to my process's generic uncaught-exception handler and does not get propagated to the promise's error/reject handler.
the code below is a sample code, which represents the issue.
Question - Is it possible for for me to handle such exceptions through the Q promise's rejection handler?
// sample code
var _inspect = require('util').inspect;
var _q = require('q');
function handle_uncaught_exception(ex) {
console.log(" Uncaught Exception: " + _inspect(ex));
process.exit(2);
}
process.on('uncaughtException', handle_uncaught_exception);
function foo_3rd_party_code() {
var d = _q.defer();
setTimeout(
function () {
throw new Error("TEST_EXCEPTION");
},
1000
);
return d.promise;
}
foo_3rd_party_code()
.then(
function (val) {
console.log("Promise Fullfilled: " + _inspect(val));
process.exit(0);
},
function (err) {
// it would be nice to see the TEST_EXCEPTION here.
console.log("Promise Rejected: " + _inspect(err));
process.exit(1);
}
);
foo_3rd_party_code can't catch or handle the exception because it can't see it. You must use the reject method of the promise for that.
function foo_3rd_party_code() {
var d = _q.defer();
setTimeout(
function () {
d.reject(new Error("TEST_EXCEPTION"));
},
1000
);
return d.promise;
}
My AngularJS code needs to chain multiple onSuccess, onError functions to a promise returned by $http.post
var promise = $http.post(url);
promise
.then(
/*success 1*/function () { console.log("success 1"); },
/*error 1*/function () { console.log("error 1"); })
.then(
/*success 2*/function () { console.log("success 2"); },
/*error 2*/function () { console.log("error 2"); });
The problem with above code is that it prints error 1 > success 2 when the HTTP response fails instead of error 1 > error 2.
I did some research on stackoverflow and found that when you have access to $q you can just do $q.reject() in error 1 to trigger error 2 but in my case i only have access to the promise returned by $http.post. So what do I do?
P.S. Of course, I can call error2() from inside of error 1 but i want to chain them because it looks more readable and extensible.
Any ideas?
Returning a value (or returning no value) from a success/error handler will resolve the promise for the next then block in the chain. To propagate the rejection, return $q.reject():
var promise = $http.post(url);
promise
.then(
/*success 1*/function () { console.log("success 1"); },
/*error 1*/function () { console.log("error 1"); return $q.reject();})
.then(
/*success 2*/function () { console.log("success 2"); },
/*error 2*/function () { console.log("error 2"); });
Your question stems from some misunderstanding of what promises enable - namely, async code composition that parallels that of a synchronous code with try/catch, with proper exception handling.
I am specifically referring to your statement:
"but i want to chain them because it looks more readable and extensible."
as the source of misunderstanding of chaining.
If your example was synchronous (assuming all async calls were blocking), this is likely what you would have wanted to do:
try {
var data = $http.post(url); // blocking
var res1 = doSuccess1(data);
var ret = doSuccess2(res1);
}
catch(e){
errorHandler1(e);
errorHandler2(e);
}
And not this:
try {
try {
var data = $http.post(url);
var res1 = doSuccess1(data);
} catch (e) {
errorHandler1(e);
// throw ""; // this is what returning $q.reject would have done - a rethrow
}
} catch (e) {
errorHandler2(e);
}
var ret = doSuccess2(res1);
which is what you would have achieved with your chaining. In other words, nested try/catch and unhandled exception in doSuccess2.
The following is the async parallel of the first approach:
var ret;
$http.post(url)
.then(function(data){
var res1 = doSuccess1(data);
ret = doSuccess2(res1);
}
.catch(function(e){ // or .then(null, handler)
doError1(e);
doError2(e);
})
And if one of doSuccessN functions were also async:
var ret;
$http.post(url)
.then(doSuccess1Async)
.then(function(res1){
ret = doSuccess2(res1);
}
.catch(function(e){ // or .then(null, handler)
doError1(e);
doError2(e);
})
Just wrap the handlers in a function, in the success / error handler parameters:
var promise = $http.post(url);
promise
.then(function(argX, argY){
success1(argX, argY);
success2(argX, argY);
},
function(argX, argY){
error1(argX, argY);
error2(argX, argY);
});