I am still fairly new to promises and am using bluebird currently, however I have a scenario where I am not quite sure how to best deal with it.
So for example I have a promise chain within an express app like so:
repository.Query(getAccountByIdQuery)
.catch(function(error){
res.status(404).send({ error: "No account found with this Id" });
})
.then(convertDocumentToModel)
.then(verifyOldPassword)
.catch(function(error) {
res.status(406).send({ OldPassword: error });
})
.then(changePassword)
.then(function(){
res.status(200).send();
})
.catch(function(error){
console.log(error);
res.status(500).send({ error: "Unable to change password" });
});
So the behaviour I am after is:
Goes to get account by Id
If there is a rejection at this point, bomb out and return an error
If there is no error convert the document returned to a model
Verify the password with the database document
If the passwords dont match then bomb out and return a different error
If there is no error change the passwords
Then return success
If anything else went wrong, return a 500
So currently catches do not seem to stop the chaining, and that makes sense, so I am wondering if there is a way for me to somehow force the chain to stop at a certain point based upon the errors, or if there is a better way to structure this to get some form of branching behaviour, as there is a case of if X do Y else Z.
Any help would be great.
This behavior is exactly like a synchronous throw:
try{
throw new Error();
} catch(e){
// handle
}
// this code will run, since you recovered from the error!
That's half of the point of .catch - to be able to recover from errors. It might be desirable to rethrow to signal the state is still an error:
try{
throw new Error();
} catch(e){
// handle
throw e; // or a wrapper over e so we know it wasn't handled
}
// this code will not run
However, this alone won't work in your case since the error be caught by a later handler. The real issue here is that generalized "HANDLE ANYTHING" error handlers are a bad practice in general and are extremely frowned upon in other programming languages and ecosystems. For this reason Bluebird offers typed and predicate catches.
The added advantage is that your business logic does not (and shouldn't) have to be aware of the request/response cycle at all. It is not the query's responsibility to decide which HTTP status and error the client gets and later as your app grows you might want to separate the business logic (how to query your DB and how to process your data) from what you send to the client (what http status code, what text and what response).
Here is how I'd write your code.
First, I'd get .Query to throw a NoSuchAccountError, I'd subclass it from Promise.OperationalError which Bluebird already provides. If you're unsure how to subclass an error let me know.
I'd additionally subclass it for AuthenticationError and then do something like:
function changePassword(queryDataEtc){
return repository.Query(getAccountByIdQuery)
.then(convertDocumentToModel)
.then(verifyOldPassword)
.then(changePassword);
}
As you can see - it's very clean and you can read the text like an instruction manual of what happens in the process. It is also separated from the request/response.
Now, I'd call it from the route handler as such:
changePassword(params)
.catch(NoSuchAccountError, function(e){
res.status(404).send({ error: "No account found with this Id" });
}).catch(AuthenticationError, function(e){
res.status(406).send({ OldPassword: error });
}).error(function(e){ // catches any remaining operational errors
res.status(500).send({ error: "Unable to change password" });
}).catch(function(e){
res.status(500).send({ error: "Unknown internal server error" });
});
This way, the logic is all in one place and the decision of how to handle errors to the client is all in one place and they don't clutter eachother.
.catch works like the try-catch statement, which means you only need one catch at the end:
repository.Query(getAccountByIdQuery)
.then(convertDocumentToModel)
.then(verifyOldPassword)
.then(changePassword)
.then(function(){
res.status(200).send();
})
.catch(function(error) {
if (/*see if error is not found error*/) {
res.status(404).send({ error: "No account found with this Id" });
} else if (/*see if error is verification error*/) {
res.status(406).send({ OldPassword: error });
} else {
console.log(error);
res.status(500).send({ error: "Unable to change password" });
}
});
I am wondering if there is a way for me to somehow force the chain to stop at a certain point based upon the errors
No. You cannot really "end" a chain, unless you throw an exception that bubbles until its end. See Benjamin Gruenbaum's answer for how to do that.
A derivation of his pattern would be not to distinguish error types, but use errors that have statusCode and body fields which can be sent from a single, generic .catch handler. Depending on your application structure, his solution might be cleaner though.
or if there is a better way to structure this to get some form of branching behaviour
Yes, you can do branching with promises. However, this means to leave the chain and "go back" to nesting - just like you'd do in an nested if-else or try-catch statement:
repository.Query(getAccountByIdQuery)
.then(function(account) {
return convertDocumentToModel(account)
.then(verifyOldPassword)
.then(function(verification) {
return changePassword(verification)
.then(function() {
res.status(200).send();
})
}, function(verificationError) {
res.status(406).send({ OldPassword: error });
})
}, function(accountError){
res.status(404).send({ error: "No account found with this Id" });
})
.catch(function(error){
console.log(error);
res.status(500).send({ error: "Unable to change password" });
});
I have been doing this way:
You leave your catch in the end. And just throw an error when it happens midway your chain.
repository.Query(getAccountByIdQuery)
.then((resultOfQuery) => convertDocumentToModel(resultOfQuery)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')
.then((model) => verifyOldPassword(model)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')
.then(changePassword)
.then(function(){
res.status(200).send();
})
.catch((error) => {
if (error.name === 'no_account'){
res.status(404).send({ error: "No account found with this Id" });
} else if (error.name === 'wrong_old_password'){
res.status(406).send({ OldPassword: error });
} else {
res.status(500).send({ error: "Unable to change password" });
}
});
Your other functions would probably look something like this:
function convertDocumentToModel(resultOfQuery) {
if (!resultOfQuery){
throw new Error('no_account');
} else {
return new Promise(function(resolve) {
//do stuff then resolve
resolve(model);
}
}
Probably a little late to the party, but it is possible to nest .catch as shown here:
Mozilla Developer Network - Using Promises
Edit: I submitted this because it provides the asked functionality in general. However it doesn't in this particular case. Because as explained in detail by others already, .catch is supposed to recover the error. You can't, for example, send a response to the client in multiple .catch callbacks because a .catch with no explicit return resolves it with undefined in that case, causing proceeding .then to trigger even though your chain is not really resolved, potentially causing a following .catch to trigger and sending another response to the client, causing an error and likely throwing an UnhandledPromiseRejection your way. I hope this convoluted sentence made some sense to you.
Instead of .then().catch()... you can do .then(resolveFunc, rejectFunc). This promise chain would be better if you handled things along the way. Here is how I would rewrite it:
repository.Query(getAccountByIdQuery)
.then(
convertDocumentToModel,
() => {
res.status(404).send({ error: "No account found with this Id" });
return Promise.reject(null)
}
)
.then(
verifyOldPassword,
() => Promise.reject(null)
)
.then(
changePassword,
(error) => {
if (error != null) {
res.status(406).send({ OldPassword: error });
}
return Promise.Promise.reject(null);
}
)
.then(
_ => res.status(200).send(),
error => {
if (error != null) {
console.error(error);
res.status(500).send({ error: "Unable to change password" });
}
}
);
Note: The if (error != null) is a bit of a hack to interact with the most recent error.
I think Benjamin Gruenbaum's answer above is the best solution for a complex logic sequence, but here is my alternative for simpler situations. I just use an errorEncountered flag along with return Promise.reject() to skip any subsequent then or catch statements. So it would look like this:
let errorEncountered = false;
someCall({
/* do stuff */
})
.catch({
/* handle error from someCall*/
errorEncountered = true;
return Promise.reject();
})
.then({
/* do other stuff */
/* this is skipped if the preceding catch was triggered, due to Promise.reject */
})
.catch({
if (errorEncountered) {
return;
}
/* handle error from preceding then, if it was executed */
/* if the preceding catch was executed, this is skipped due to the errorEncountered flag */
});
If you have more than two then/catch pairs, you should probably use Benjamin Gruenbaum's solution. But this works for a simple set-up.
Note that the final catch only has return; rather than return Promise.reject();, because there's no subsequent then that we need to skip, and it would count as an unhandled Promise rejection, which Node doesn't like. As is written above, the final catch will return a peacefully resolved Promise.
I wanted to preserve the branching behaviour that Bergi's answer had, yet still provide the clean code structure of unnested .then()'s
If you can handle some ugliness in the machinery that makes this code work, the result is a clean code structure similar to non-nested chained .then()'s
One nice part of structuring a chain like this, is that you can handle all the potential results in one place by chainRequests(...).then(handleAllPotentialResults) this might be nice if you need to hide the request chain behind some standardised interface.
const log = console.log;
const chainRequest = (stepFunction, step) => (response) => {
if (response.status === 200) {
return stepFunction(response, step);
}
else {
log(`Failure at step: ${step}`);
return response;
}
};
const chainRequests = (initialRequest, ...steps) => {
const recurs = (step) => (response) => {
const incStep = step + 1;
const nextStep = steps.shift();
return nextStep ? nextStep(response, step).then(chainRequest(recurs(incStep), incStep)) : response;
};
return initialRequest().then(recurs(0));
};
// Usage
async function workingExample() {
return await chainRequests(
() => fetch('https://jsonplaceholder.typicode.com/users'),
(resp, step) => { log(`step: ${step}`, resp); return fetch('https://jsonplaceholder.typicode.com/posts/'); },
(resp, step) => { log(`step: ${step}`, resp); return fetch('https://jsonplaceholder.typicode.com/posts/3'); }
);
}
async function failureExample() {
return await chainRequests(
() => fetch('https://jsonplaceholder.typicode.com/users'),
(resp, step) => { log(`step: ${step}`, resp); return fetch('https://jsonplaceholder.typicode.com/posts/fail'); },
(resp, step) => { log(`step: ${step}`, resp); return fetch('https://jsonplaceholder.typicode.com/posts/3'); }
);
}
console.log(await workingExample());
console.log(await failureExample());
The idea is there, but the interface exposed could probably use some tweaking.
Seeing as this implementation used curried arrow functions, the above could potentially be implemented with more direct async/await code
Related
So I want to know how to make sure that my Node application will crash if it comes upon a programmer error(undefined variable, reference error, syntax error...). However, if I am using promise chains then the final catch() will catch all possible errors including programmer errors.
For example:
PromiseA()
.then((result) => {
foo.bar(); //UNDEFINED FUNCTION HERE!!!!!
return PromiseB(result);
})
.catch(
//handle errors here
)
Now the catch() statement will also catch the really bad error of an undefined function and then try to handle it. I need a way for my program to crash when it comes up against errors like these.
EDIT: I also just realized that even if I throw an error in the last catch it will just be consumed by the promise chain :-[. How am I supposed to deal with that?
Basically, what you want to do is to handle those errors that you can potentially recover from. These errors are usually something you throw in your code. For example, if an item is not found in a DB some libraries will throw an Error. They'll add a type property or some other property to differentiate the different type of errors.
You can also potentially subclass the Error class and use instanceof to differentiate each error.
class myOwnError extends Error {}
Then:
Prom.catch(err => {
if(err instanceof myOwnError){ /* handle error here */ }
else { throw err; }
});
If you want to avoid if/chains, you can use a switch on error.constructor:
switch(err.constructor){
case myOwnError:
break;
case someOtherError:
break;
default:
throw err;
}
You can also use an a Map or regular objects by creating functions for each possible error and storing them. With a Map:
let m = new Map();
m.set(myOWnError, function(e){ /*handle error here*/ });
m.set(myOtherError, function(e){ /*handle other error here*/ });
Then just do:
Prom.catch(err => {
let fn = m.get(err.constructor);
if(fn){ return fn(err); }
else { throw err; }
});
Disclaimer: Below is a description what we do at the company I work. The package linked is written by us.
What we do is to catch all errors and sort them into programmer and operational errors.
We've made small library to help us: https://www.npmjs.com/package/oops-error
For promise chains we use:
import { programmerErrorHandler } from 'oops-error'
...
export const doSomething = (params) => {
somePromiseFunction().catch(programmerErrorHandler('failed to do something', {params}))
}
It marks the error as programmer error, adds 'failed to do something' as error message and adds the params as a context (for debugging later)
For errors that we know that can come up (person not found, validEmail, etc) we do something like
import { Oops } from 'oops-error'
export const sendEmail = (email) => {
if(!isValidEmail(email)) {
throw new Oops({
message: 'invalid email',
category: 'OperationalError',
context: {
email,
},
})
}
...
}
At every level we show the error messages of Operational Errors. So a simple
.cath(e => {
if (e.category == 'OperationalError') {
// handle the gracefully
}
else {
throw e // We will tackle this later
}
}
And at the end of our request in express we have our final error handler, where catch the error, check if its operational and then show the error message, but not the actual context. If its a programmer error we stop the process (not ideal, but we don't want the user to keep messing with broken code)
I know that there are answers out there but I didn't find a specific answer to my actual question.
Currently I use the following pattern a lot :
class A
{
getLocation()
{
return Promise.reject(2222222)
}
async a()
{
try
{
var loc = await this.getLocation();
alert(loc)
}
catch (e)
{
alert("catch 2")
}
}
}
new A().a();
Result : "catch 2"
Event If I throw an error in getLocation :
getLocation()
{
throw Error("ffffff")
}
- I get the same result - which is OK.
So where is the problem ?
Well as you know , an error which is thrown asynchronously-non-promised is a different beast :
So this code won't be catched at all:
getLocation() //bad code from a third party code , third party code
{
return new Promise((v, x) => setTimeout(() =>
{
throw Error("ffffff")
}, 100))
}
Question :
Regarding the fact that I want to catch errors - is there a better pattern for capturing this ?
Sure I can do :
window.onerror = function () { alert(4)}
But that would be not in order as the flow of .catch(...) or catch(){} , and I won't be able to do actions regarding that specific action that caused error.
Full disclosure:
No real life scenario. Learning purpose .
an error which is thrown asynchronously-non-promised is a different beast
Yes. And it must be avoided at all costs. Therefore, never put business code (including trivial things like property accesses) in asynchronous non-promise callbacks. It could throw! It should be obvious that JSON.parse can fail, that a property access can throw when the "object" is null or undefined or a getter is involved, or that a loop can fail when the thing that was supposed to be an array has no .length.
The only things that are allowed as asynchronous non-promise callbacks are resolve, reject, and (err, res) => { if (err) reject(err); else resolve(res); } (and maybe a variadic version for weird APIs with multiple arguments).
So rewrite the bad code to
async getLocation() {
await new Promise(resolve => setTimeout(resolve, 100));
throw Error("ffffff");
}
or
getLocation() {
return new Promise(resolve => setTimeout(resolve, 100)).then(res => {
throw Error("ffffff");
});
}
and when it's third-party code make them fix it, make an upstream merge request of your fix, or if those don't work abandon that party.
is there a better pattern for capturing this?
Well, domains (in node) were supposed to solve this problem of non-locality of asynchronous (uncaught) exceptions, but they didn't work out. Maybe some day, zones with better native language support will replace them.
The errors should be caught in place where they occur.
This kind of code code is incorrect and should be fixed in-place:
getLocation() //bad code from a third party code
{
return new Promise((v, x) => setTimeout(() =>
{
throw Error("ffffff")
}, 100))
}
If this is third-party code, it can be forked or patched.
Exceptions can be tracked globally by onerror, as the question already mentions. This should be used only to notify a developer of existing errors, not to handle them in normal way.
unhandledrejection event can be used for same purpose to notify about unhandled rejections in promises. It won't be able to handle the error in the snipped above because it is thrown inside setTimeout callback and doesn't result in promise rejection.
I guess the basic usage would be like this:
class A {
getLocation(x) {
return new Promise((resolve, reject) => setTimeout(() => {
// a lot of async code
try {
//simulate unexpected exception
if (x === 2) {
throw ("code error");
}
if (x) {
resolve('success');
} else {
reject('conditional rejection');
}
} catch (ex) {
reject(ex);
}
}, 1000));
}
async a(x) {
await this.getLocation(x).then((loc) => console.info(loc)).catch((e) => console.error(e));
}
}
let o = new A();
o.a(2);
o.a(0);
o.a(1);
The rejection of the Promise is not necessarily an code Exception as well as the code Exception should not necessarily trigger a Promise rejection.
I think I'm preventing nested queries as much as possible, but I'm honestly not sure. I understand the calls here can all be executed in a single select query, but I did this to simplify the example.
// This example is in TypeScript
// find user
User.find({where:{username:'user'}})
// if found user
.then(function(user) {
return User.find({where:{username:'other_user'}})
// if found other_user
.then(function(other_user) {
// do stuff
return whatever_i_need
}
// if something went wrong, go straight to parent catch
.catch(function(err) {
// do stuff
throw new Error()
}
}
// if previous .then() returned success
.then(function(data) {
return User.find({where:{username:'yet_another_user'}})
// if found yet_another_user
.then(function(yet_another_user) {
// do stuff
return whatever_i_need_again
}
// if something went wrong, go straight to parent catch
.catch(function(err) {
// do stuff
throw new Error()
}
}
// if anything threw an error at any point in time
.catch(function(err) {
// handle the error
}
However, this results in nested promises, which is exactly what promises are meant to prevent. Is this the "max depth" recommended for promises, or am I missing something? Is there a better way to chain queries?
Return the nested promise instead of handling it in the inner blocks to flatten the structure.
User.find({where:{username:'user'}})
.then(function(user) {
if (user) { // if found user
// do stuff
return User.find({where:{username:'other_user'}});
}
throw new Error('user not-found');
})
.then(function(other_user) {
if (other_user) { // if found other_user
// do stuff
return whatever_i_need;
}
throw new Error('other_user not-found');
})
.then(function(data) {
return User.find({where:{username:'yet_another_user'}})
})
.then(function(yet_another_user) {
if (yet_another_user) { // if found yet_another_user
// do stuff
return whatever_i_need_again;
}
throw new Error('yet_another_user not-found');
}
.then(function(data){
// do stuff
})
.catch(function(err) { // if anything threw an error at any point in time
// handle the error
}
Note that a resolved promise means a query is successfully done. That's it all about. A successful query does't guarantee results to be returned. Empty result is a valid outcome of resolved promises.
Note also that the return value from a resolve or reject callback will be wrapped with a resolved promise, and then passed to the next then block, making a meaningful promise chain. Thanks for #Matt's follow-up feedback below regarding this point.
Two points:
Drop .catch(function(err) { throw new Error() }. It does nothing but remove the error message.
You can unnest the inner then calls
So it just should be
User.find({where:{username:'user'}})
.then(function(user) {
return User.find({where:{username:'other_user'}})
})
.then(function(other_user) {
// do stuff
return whatever_i_need
})
// if previous .then() returned success
.then(function(data) {
return User.find({where:{username:'yet_another_user'}})
})
// if found yet_another_user
.then(function(yet_another_user) {
// do stuff
return whatever_i_need_again
})
// if anything threw an error at any point in time
.catch(function(err) {
// handle the error
})
I am trying to use Mongoose's built in promise support to write some clean Javascript code for a user sending a friend request to another. However, when I try to ensure proper error handling and sequentiality, I still end up with a (slightly smaller than normal) pyramid of doom.
Here, I first ensure that the friend request is valid, then save the target's Id to the requester's sent requests then, if that save was successful, save the requester's Id to the target's friend requests.
Do I need to use a third party library like q in order to do this as cleanly as possible? How can I structure this such that I can use the traditional single error handler at the end?
function _addFriend (requesterId, targetId) {
// (integer, integer)
User.findById(requesterId)
.exec((requester) => {
if (!(targetId in requester.friends
|| targetId in requester.sentfriendRequests
|| targetId in requester.friendRequests)) {
requester.sentfriendRequests = requester.sentfriendRequests.concat([targetId])
requester.save()
.then((err) => {
if (err) throw err;
User.findById(targetId)
.exec((err, target) => {
if (err) throw err;
target.friendRequests = target.friendRequests.concat([requesterId])
target.save().then(err => {if (err) throw err})
})
})
}
})
}
You will need some nesting to do conditionals in promise code, but not as much as with callback-based code.
You seem to have messed up a bit of the if (err) throw err; stuff, you should never need that with promises. Just always use .then(result => {…}), and don't pass callbacks to exec any more.
If you always properly return promises from your asynchronous functions (including then callbacks for chaining), you can add the single error handler in the end.
function _addFriend (requesterId, targetId) {
// (integer, integer)
return User.findById(requesterId).exec().then(requester => {
if (targetId in requester.friends
|| targetId in requester.sentfriendRequests
|| targetId in requester.friendRequests) {
return;
}
requester.sentfriendRequests = requester.sentfriendRequests.concat([targetId])
return requester.save().then(() => {
return User.findById(targetId).exec()
}).then(target => {
target.friendRequests = target.friendRequests.concat([requesterId])
return target.save()
});
});
}
_addFriend(…).catch(err => {
…
})
In English, the way to do this is to use the promises returned by exec() have then blocks return promises, un-indent, then add then to those. Much easier to say in code...
EDIT thanks (again) to #Bergi for making me read and understand the app logic. #Bergi is right that there must be a little nesting to get the job done, but the real point isn't about reducing nesting, but about improving clarity.
Better clarity can come from factoring into logical parts, including some that return in promises.
These few functions conceal the promise nesting that's required by the logic. This doesn't specify (because the OP doesn't indicate how the app should handle) what addFriend should return when it refuses to do so due to an existing request...
function _addFriend (requesterId, targetId) {
// note - pass no params to exec(), use it's returned promise
return User.findById(requesterId).exec().then((requester) => {
return canAddFriend(requester, targetId) ? addFriend(requester, targetId) : null;
});
}
function canAddFriend(requester, targetId) {
return requester && targetId &&
!(targetId in requester.friends
|| targetId in requester.sentfriendRequests
|| targetId in requester.friendRequests);
}
function addFriend(requester, targetId) {
requester.sentfriendRequests = requester.sentfriendRequests.concat([targetId]);
return requester.save().then(() => {
return User.findById(targetId).exec();
}).then((target) => {
target.friendRequests = target.friendRequests.concat([requesterId]);
return target.save();
});
}
Once you realise that .exec() returns a promise, you can :
achieve the desired flattening and make the code more readable.
avoid the need to handle errors amongst the "success" code.
handle errors in a terminal .then() or .catch().
As a bonus you can also (more readily) throw meaningful errors for each of those x in y conditions.
Straightforwardly, you could write :
function _addFriend(requesterId, targetId) {
return User.findById(requesterId).exec().then(requester => {
if (targetId in requester.friends) {
throw new Error('target is already a friend');
}
if (targetId in requester.sentfriendRequests) {
throw new Error('friend request already sent to target');
}
if (targetId in requester.friendRequests) {
throw new Error('target already sent a friend request to requester');
}
requester.sentfriendRequests = requester.sentfriendRequests.concat([targetId]); // or just .push()?
return requester.save();
}).then(() => {
return User.findById(targetId).exec().then(target => {
target.friendRequests = target.friendRequests.concat([requesterId]); // or just .push()?
return target.save();
});
});
}
Note the need for returns to control flow.
But you could do even better. As writtten above, the requested stuff could succeed then the target stuff fail, resulting in a db disparity. So what you really want is a db transaction to guarantee that both happen or neither. Mongoose undoubtedly provides for transactions however you can do something client-side to give you something transaction-like with partial benefit.
function _addFriend(requesterId, targetId) {
return Promise.all([User.findById(requesterId).exec(), User.findById(targetId).exec()]).then(([requester, target]) => { // note destructuring
if (targetId in requester.friends) {
throw new Error('target is already a friend');
}
if (targetId in requester.sentfriendRequests) {
throw new Error('friend request already sent to target');
}
if (targetId in requester.friendRequests) {
throw new Error('target already sent a friend request to requester');
}
requester.sentfriendRequests = requester.sentfriendRequests.concat([targetId]);
target.friendRequests = target.friendRequests.concat([requesterId]);
return requester.save().then(() => {
return target.save();
});
});
}
Here, you could still get the (unlikely) situation that the first save is successful and the second save fails, but at least you have the assurance that absolutely nothing happens unless both the requester and target exist.
In both cases, call as follows :
_addFriend(requesterId, targetId).then(function() {
// do whatever on success
}, function(error) {
// do whatever on error
});
Even if you don't use the error messages in the live environment, they could be very useful when testing/debugging. Please check them - I may have gotten them wrong.
I'm using promise library Bluebird in all my Node.js projects. For getting the content of the first existent file from a list of file paths I use Promise.any successfully as follows:
Promise.any([
'./foo/file1.yaml',
'./foo/file2.yaml',
'./foo/file3.yaml'
], function(filePath) {
_readFile(filePath);
}),then(function(fileContent) {
_console.log(fileContent);
});
My question is, how can i leave the Promis.any loop early if i get an error which is different from "file not found", when reading a file? The following code illustrates my question:
Promise.any([
'./foo/file1.yaml',
'./foo/file2.yaml',
'./foo/file3.yaml'
], function(filePath) {
_readFile(filePath)
.catch(function(err) {
var res = err;
if (err.code == FILE_NOT_FOUND) {
// continue the Promise.any loop
} else {
// leave the Promise.any loop with this error
err = new Promise.EarlyBreak(err);
}
Promise.reject(err);
});
}).then(function(fileContent) {
_console.log(fileContent);
}, function(err) {
// the first error different from FILE_NOT_FOUND
});
May be Promise.any is not the right function?
Leaving a Promise.any() loop early is conceptually problematic in that Promise.any() is an aggregator not a loop, and accepts an array of promises, each of which has a life of its own, not determined by Promise.any().
However, starting with an array of paths, the loop you seek can be expressed as a paths.reduce(...) expression, which builds a .catch() chain, straightforwardly as follows :
function getFirstGoodFileContent(paths) {
paths.reduce(function(promise, path) {
return promise.catch(function() {
return _readFile(path);
});
}, Promise.reject()); // seed the chain with a rejected promise.
}
Catch chain: credit Bergi
The .catch chain thus built, will progress to the next iteration on failure, or skip to the end of the chain on success. This flow control is the inverse of what happens in a more normal .then chain (seeded with a fulfilled promise).
But that's not quite everything. An extra condition is required - namely to "leave the [Promise.any] loop early if I get an error which is different from 'file not found'". This is very simply engineered into the catch chain by sending all errors except FILE_NOT_FOUND down the success path, thereby :
effecting the required flow control (skipping the rest of the chain), but
ending up with an error condition going down the success route - undesirable but recoverable.
function getFirstGoodFileContent(paths) {
paths.reduce(function(promise, path) {
return promise.catch(function() {
return _readFile(path).catch(function(err) {
if (err.code == FILE_NOT_FOUND) {
throw err; // Rethrow the error to continue down the catch chain, seeking a good path.
} else {
return { isError: true, message: err.code }; // Skip the rest of the catch chain by returning a "surrogate success object".
}
});
});
}, Promise.reject()).then(function(fileContent) {
// You will arrive here either because :
// * a good path was found, or
// * a non-FILE_NOT_FOUND error was encountered.
// The error condition is detectable by testing `fileContent.isError`
if (fileContent.isError) {
throw new Error(fileContent.message); // convert surrogate success to failure.
} else {
return fileContent; // Yay, genuine success.
}
});
}
So you can now call :
getFirstGoodFileContent([
'./foo/file1.yaml',
'./foo/file2.yaml',
'./foo/file3.yaml'
]).then(function(fileContent) {
_console.log(fileContent);
}, function(error) {
// error will be due to :
// * a non-FILE_NOT_FOUND error having occurred, or
// * the final path having resulted in an error.
console.log(error);
});