How can I eliminate these nested Promises? - javascript

I've read that you should avoid nested promises in JavaScript, as they tend to be an antipattern, but I'm having trouble figuring out how to avoid them in my particular use case. Hopefully someone with more experience than me can see where I'm going wrong? Any advice would be much appreciated!!
Essentially, I'm retrieving some data asynchronously, processing it and catching a possible Error, and then saving some data asynchronously. Here's a much-simplified example:
class Foo {
changeName(path, newName) {
this.getPage(path) // AJAX call, returns a promise with type Page,
// may throw an Error if the page does not exist.
.then(page=> {
// Some modifications are made to page, omitted here
return page
})
.catch(e=>{
if(e instanceof PageDoesNotExistError) {
return new Page();
}
})
.then(page=> {
page.name = newName;
this.savePage(path, page); // ******
// I want my outer changeName method to return this ^ promise,
// or at least a promise that will resolve when this promise
// resolves, with the same value.
})
}
}
How can I have changeName return a promise that will resolve with the value of this.savePage (the line marked with //******), so that I could do something like this elsewhere:
myFooInstance.changeName('/path', 'New Name').then(page=> {
// Do something with the returned saved page
});

How can I have changeName return a promise that will resolve with the value of this.savePage
Return the promise savePage returns from the then handler. That will resolve the promise created by then to the promise from savePage, meaning that the promise created by then will be fulfilled or rejected depending on what the promise from savePage does. (Details on that terminology in my blog post here if you're interested.)
.then(page=> {
page.name = newName;
return this.savePage(path, page);
})
Separately, you've said you want the caller to be able to use the promise on the return value of changePage but you're not returning anything out of changePage. You need to add return in front of the whole structure so you return the ultimate promise.
changeName(path, newName) {
return this.getPage(path) // AJAX call, returns a promise with type Page,
// ...
(See below for a complete version.)
Side note: You have an error waiting to happen here:
.catch(e=>{
if(e instanceof PageDoesNotExistError) {
return new Page();
}
})
If e is not an instance of PageDoesNotExistError, you're converting the rejection into a resolution with the value undefined, because your catch handler doesn't return an explicit value in that case. If you want to propagate the error instead, you need to do that with throw e or return Promise.reject(e):
.catch(e=>{
if(e instanceof PageDoesNotExistError) {
return new Page();
}
return Promise.reject(e);
})
Bringing all three of those things together:
class Foo {
changeName(path, newName) {
return this.getPage(path)
.then(page=> {
// Some modifications are made to page, omitted here
return page;
})
.catch(e=>{
if(e instanceof PageDoesNotExistError) {
return new Page();
}
return Promise.reject(e);
})
.then(page=> {
page.name = newName;
return this.savePage(path, page);
});
}
}

You would just return the promise and in turn return from any nested promise:
class Foo {
changeName(path, newName) {
return this.getPage(path)
.catch(e => {
if(e instanceof PageDoesNotExistError) {
return new Page();
}
})
.then(page=> {
page.name = newName;
return this.savePage(path, page); // assuming that this is a promise
})
}
}

Related

Why then is always called with undefined?

I have following chain of promises:
return new Promise((resolve, reject) => {
isUserExists(user.username, user.email).then((existResult) => {
/// already exists
if (existResult.rows[0].exists == true) {
reject('Already exists');
} else {
return insertCity({
city_id: user.city_id,
city_name: user.city_name
});
}
}).then((cityResult) => {
/// this is one is always called even I didn't called insertCity above
console.log(cityResult); /// could be undefined if not called
return insertUser(user); /// I don't want this to be executed
});
});
If user already exists, I call reject('Already exists'). But anyway, next then is called anyway, which means insertUser is called also. I don't want it to happen. I just want to stop everything when reject is called at any point. return reject(Already exists) does not help. How can I solve the problem? It seems I don't understand core thing of Promises.
Couple of problems in your code:
You don't need to wrap isUserExists(user.username, user.email) in a Promise constructor because it already returns a Promise.
Inside the callback function of first .then() method, calling reject() rejects the newly created Promise but it doesn't stops the execution of the callback function of the first .then() method until it implicitly returns undefined which then leads to the invocation of the second .then() block.
Solution
You don't need a Promise constructor. Once you have determined that user already exists, you could throw an error.
return isUserExists(user.username, user.email)
.then((existResult) => {
/// already exists
if (existResult.rows[0].exists == true) {
throw new Error("already exists");
}
return insertCity({ city_id: user.city_id, city_name: user.city_name });
})
.then((cityResult) => {
console.log(cityResult);
return insertUser(user);
});
Make sure to add the .catch() block in the calling code to handle the error when the user already exists.
You have two independant promises.
If existResult.rows[0].exists == true then you reject the outer promise.
That doesn't effect the inner promise, which hasn't been rejected. So it's then continues.
Don't nest promises.
Throw an exception if you want to trigger a rejection chain from inside a promise chain.
function isUserExists() {
return Promise.resolve("OK")
}
function getPromise() {
return isUserExists().then(function() {
// it exists!
if (true) {
throw "User exists.";
}
}).then(function() {
console.log("This won't be called");
});
}
getPromise().then(function() {
console.log("Also won't be called");
}).catch(function() {
console.log("Error here!");
});
This is still rather messy though. You'd be better off using await and async syntax:
async function example() {
const existResult = await isUserExists(user.username, user.email);
if (existResult.rows[0].exists == true) throw "Already exists";
const cityResult = await insertCity({
city_id: user.city_id,
city_name: user.city_name
});
console.log(cityResult);
return insertUser(user);
}

Why can I return a recursive run of promises?

I have the following code (reconstructed almost 1:1 to work in here):
First of all, understand that bridge_component_start_test is really a call to a constructor object where we generate the initial promise by return new Promise.. I tried to mimick this here. It's my factory for AJAX calls, so, each object creates a promise, which makes a call, which then resolves / rejects with the JSON that it received from the server. I've emulated that response in the resolves below as well.
function recursive_function(data) {
let local_data = $.extend({}, data);
return new Promise((resolve, reject) => {
//The offset gets
local_data.bridge_process_batch_offset++;
if (local_data.bridge_process_batch_offset == 3) {
resolve({
'data': {
'response_data': {
'done_batching': true
}
},
'success': true
})
} else {
resolve({
'data': {
'response_data': {
'done_batching': false,
'bridge_process_batch_offset': local_data.bridge_process_batch_offset
}
},
'success': true
})
}
//For now, no reject.
});
}
function do_the_main_thing(data) {
/**
* Make a local copy of the passed data.
*/
let request_data = $.extend({}, data);
let bridging_data = {
'just_installed_component_demo': request_data.just_installed_component_demo,
'just_installed_component_name': request_data.just_installed_component_name,
'just_installed_component_category': request_data.just_installed_component_category,
'bridge_process_batch_offset': 0
};
const recursively_do_things = () => recursive_function(bridging_data).then(response => {
if (response.data.response_data.done_batching) {
return (response);
} else if (response.data.success == false) {
return response;
} else {
console.log('Went through step ' + response.data.response_data.bridge_process_batch_offset + '!');
if ('bridge_process_batch_offset' in response.data.response_data) {
bridging_data.bridge_process_batch_offset = response.data.response_data.bridge_process_batch_offset;
}
return recursively_do_things ();
}
}).catch(error => {
return error;
});
return recursively_do_things();
}
do_the_main_thing({
'just_installed_component_demo': 'demo-2',
'just_installed_component_name': 'demo-2_post',
'just_installed_component_category': 'post',
'bridge_process_batch_offset': 0
}).then(response => {
console.log('...and now I am done!');
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
But notice, in my do_the_main_thing I'm merely returning the response from that promise recursion. Yes, surely, I also return recursively_bridge_component_start but how does JS know to return a promise out of that recursive functionality?
A return is a return. It doesn't have to wait for the Promise to finish, yet it seems it does. In theory, my last line of return recursively_do_things should just instantly return before the Promises are done.
What am I missing here?
But it's quite straightforward. The return value of bridge_component_start_in_test(data) is the return value of recursively_bridge_component_start().
That function in turn returns bridge_component_start_test(data).then(/* do stuff */).catch(/* do other stuff */). That will always be a promise regardless of details of "do stuff" and "do other stuff".
And bridge_component_start_test explicitely returns a promise: return new Promise(//...
You are not returning merely response from recursively_do_things
If you look at the function, you are returning a part of the chain, your return response is just part of the promise chain.
So this part
const recursively_do_things= () => recursive_function(bridging_data).then(response => /* omitted */ );
is returning you a promise chain implying that it first waits for recursive_function to complete, and then does the inner response, after which you can chain other statements
so, since you are returning return recursively_do_things() from your do_the_main_thing(data), it is clear that a promise will be returned.
Update based on comment
Promises are promise aware themselves, which means that if you return a promise in a chain, the promise chain will wait for the returned promise to complete before continuing the next part of the chain.
The requirement for that is that you need to return it, if you just call a function that returns a promise, it will not wait for it.

Bluebird.each break if solved

I want to test each element of an array until a condition is met then skip the rest. This is the code I came up with, it seems to be working but I'm not sure if its actually safe or it has unexpected side effects. Other solutions are welcomed.
let buddyAdded = false;
replicaArr = _.keys(ReplicaList);
Promise.each(replicaArr, function(replicaname) {
if (!buddyAdded) {
Player.count({
'buddyList': replicaname
}, function(err, result) {
if (err) {
} else if (result < 990) {
Player.update({
'_id': buddyObj.buddyId
}, {
'buddyList': replicaname
}, function(err) {
if (err) {
} else {
ReplicaList[replicaname].send(buddyObj);
buddyAdded = true;
// success stop here and skip all the other array elements
return;
}
});
}
});
}
});
If you're trying to enumerate the players serially one at a time and abort the iteration when you find a player with room in their buddy list that you can update the list and communicate back any errors that happen, then here's one way of doing it.
Here's how this works:
Use Bluebird's Promise.promisifyAll() to automatically make promise returning methods for the Player object so we can then use those in our control flow.
Use Bluebird's Promise.mapSeries() to iterate the array serially one at a time.
Chain the Player.countAsync() and Player.updateAsync() methods so they sequence properly and return them from .mapSeries() so it waits for them to complete before continuing the iteration to the next array element.
If we find a player with room and successfully update its buddy list, then throw a specially exception. This will reject the current promise chain and cause .mapSeries() to stop it's iteration (which is what you said you wanted).
Add a .catch() at the higher level that tests for the special rejection and turns it into a successful resolved promise. If it's some other error, then let that continue to propagate as an actual error.
The code:
// Promisify the Player object so the methods
// this would usually be done wherever this module is loaded
Player = Promise.promisifyAll(Player);
// create a special subclass of Error for our short circuit
PlayerUpdateDone extends Error {
constructor(name) {
super();
this.name = name;
}
}
// put logic into a function to make it cleaner to use
function addToBuddyList(replicaArr) {
return Promise.mapSeries(replicaArr, function(replicaname) {
return Player.countAsync({buddyList: replicaname}).then(function(result) {
// if room left in buddy list
if (result < 990) {
return Player.updateAsync({'_id': buddyObj.buddyId}, {'buddyList': replicaname}).then(function() {
ReplicaList[replicaname].send(buddyObj);
// throw special exception to abort .mapSeries()
// and stop further processing
throw new PlayerUpdateDone(replicaname);
});
}
});
}).then(function() {
// if it gets here, there were no players with rooms so just return null
return null;
}).catch(function(result) {
// we used a special rejection as a shortcut to stop the mapSeries from executing
// the rest of the series
if (result instanceof PlayerUpdateDone) {
// change this special rejection into a result
return result.name;
}
// must have been regular error so let that propagate
throw result;
});
}
// sample usage
addToBuddyList(replicaArr).then(function(name) {
if (name) {
console.log(`Updated player ${name}`);
} else {
console.log("No players with room in their buddy list");
}
}).catch(function(err) {
// error here
console.log(err);
});
It may be simpler to make your own sequencer that stops when the first promise resolves to a truthy value:
// fn will be passed each array element in sequence
// fn should return a promise that when resolved with a truthy value will stop the iteration
// and that value will be the resolved value of the promise that is returned from this function
// Any rejection will stop the iteration with a rejection
Promise.firstToPassInSequence = function(arr, fn) {
let index = 0;
function next() {
if (index < arr.length) {
return Promise.resolve().then(function() {
// if fn() throws, this will turn into a rejection
// if fn does not return a promise, it is wrapped into a promise
return Promise.resolve(fn(arr[index++])).then(function(val) {
return val ? val : next();
});
});
}
// make sure we always return a promise, even if array is empty
return Promise.resolve(null);
}
return next();
};
Promise.firstToPassInSequence(replicaArr, function(replicaname) {
return Player.countAsync({buddyList: replicaname}).then(function(result) {
// if room left in buddy list
if (result < 990) {
return Player.updateAsync({'_id': buddyObj.buddyId}, {'buddyList': replicaname}).then(function() {
ReplicaList[replicaname].send(buddyObj);
return replicaname;
});
}
});
});

How to fix the promise anti pattern - Ghost Promise?

I found the term "The Ghost Promise" here, which looks like my case.
I have the code like this:
return Q.Promise(function(resolve, reject) {
firstFunctionThatReturnPromise()
.then(function(firstResult) {
_check(firstResult) ? resolve(firstResult) : return secondFunctionThatReturnPromise();
})
.then(function(secondResult) {
console.log(secondResult);
return thirdFunctionThatReturnPromise(secondResult);
})
.then(function(thirdResult) {
resolve(thirdResult);
})
.catch(function(e) {
reject(e)
});
});
The problem is, even though the _check returns true, it still proceeds to the console.log command (which results in undefined).
In case the _check returns false, things work as expected.
So my question is:
If the behavior described above is normal?
If there is a more elegant way to handle this case?
Update 1: Many questioned that why I use Q.Promise instead of returning the result directly. It's because this is a generic function, shared by other functions.
// Usage in other functions
genericFunction()
.then(function(finalResult) {
doSomething(finalResult);
})
.catch(function(err) {
handleError(err);
});
First off, there's no reason to wrap a new promise around any of this. Your operations already return promises so it is an error prone anti-pattern to rewrap them in a new promise.
Second off, as others have said, a .then() handler has these choices:
It can return a result which will be passed to the next .then() handler. Not returning anything passes undefined to the next .then() handler.
It can return a promise whose resolved value will be passed to the next .then() handler or rejected value will be passed to the next reject handler.
It can throw which will reject the current promise.
There is no way from a .then() handler to tell a promise chain to conditionally skip some following .then() handlers other than rejecting.
So, if you want to branch your promise based on some condition logic, then you need to actually nest your .then() handlers according to your branching logic:
a().then(function(result1) {
if (result1) {
return result1;
} else {
// b() is only executed here in this conditional
return b().then(...);
}
}).then(function(result2) {
// as long as no rejection, this is executed for both branches of the above conditional
// result2 will either be result1 or the resolved value of b()
// depending upon your conditional
})
So, when you want to branch, you make a new nested chain that lets you control what happens based on the conditional branching.
Using your psuedo-code, it would look something like this:
firstFunctionThatReturnPromise().then(function (firstResult) {
if (_check(firstResult)) {
return firstResult;
} else {
return secondFunctionThatReturnPromise().then(function (secondResult) {
console.log(secondResult);
return thirdFunctionThatReturnPromise(secondResult);
})
}
}).then(function (finalResult) {
console.log(finalResult);
return finalResult;
}).catch(function (err) {
console.log(err);
throw err;
});
Even if this is inside a genericFunction, you can still just return the promise you already have:
function genericFunction() {
return firstFunctionThatReturnPromise().then(function (firstResult) {
if (_check(firstResult)) {
return firstResult;
} else {
return secondFunctionThatReturnPromise().then(function (secondResult) {
console.log(secondResult);
return thirdFunctionThatReturnPromise(secondResult);
})
}
}).then(function (finalResult) {
console.log(finalResult);
return finalResult;
}).catch(function (err) {
console.log(err);
throw err;
});
}
// usage
genericFunction().then(...).catch(...)
The behavior is expected. When you chain your .then() statements, you cannot break out of the chain early except by throwing an error.
Your top-level promise (the one returned by Q.Promise()) gets resolved after _check(); but you actually have an inner promise chain that continues to execute.
By specification, then() returns a promise: https://promisesaplus.com/#point-40
You can see for yourself in the source code of Q: https://github.com/kriskowal/q/blob/v1/q.js#L899
For your desired behavior, you'll actually need another nested promise chain.
return Q.Promise(function(resolve, reject) {
firstFunctionThatReturnPromise().then(function(firstResult) {
if (_check(firstResult)) {
resolve(firstResult);
} else {
return secondFunctionThatReturnPromise().then(function(secondResult) {
console.log(secondResult);
return thirdFunctionThatReturnPromise(secondResult);
});
}
});
});
I have never used Q, but everything a promise returns is transformed into a promise and passed to the next .then(). Here, your first .then() don't return anything. So it returns undefined. So undefined is wrapped in a new Promise and passed to the next handler, where you get secondResult == undefined.
You can see it in action in the following CodePen : http://codepen.io/JesmoDrazik/pen/xOaXKE

Is there a shortcut to define and return a rejected promise? [duplicate]

My scenario
I used to have some node.js implementation done using callbacks but I am now refactoring my code to use Promises instead - using Q module. I have the following update() function where the inner _update() function already returns a Promise:
exports.update = function(id, template, callback) {
if (!_isValid(template)){
return callback(new Error('Invalid data', Error.INVALID_DATA));
}
_update(id, template) // this already returns a promise
.then(function() {
console.log('UPDATE was OK!');
callback();
}, function(err) {
console.log('UPDATE with ERRORs!');
callback(err);
});
};
My question
I would like to achieve something like the following:
exports.update = function(id, template) {
if (!_isValid(template)){
// how could I make it return a valid Promise Error?
return reject(new Error('Invalid data', Error.INVALID_DATA));
}
return _update(id, template) // return the promise
.done();
};
Because _update() already returns a promise, I guess changing it this way would be enough (wouldn't be?):
return _update(id, template)
.done();
And... what about if the condition inside the if-clause equals true? How could I refactor
return callback(new Error('Invalid data', BaboonError.INVALID_DATA));
to throw an error to avoid passing the callback into update() and handling that error (or what ever error could ever be returning _update())?
Also, calling update():
myModule.update(someId, someTemplate)
.then(function() { /* if the promise returned ok, let's do something */ })
.catch(function(err) { /* wish to handle errors here if there was any */});
somewhere else in my code:
if there is an error during the promise propagation - it should handle it,
or, if there wasn't an error - it should do some other things
Am I close to what I am expecting? How could I finally achieve it?
I see only two problems.
If you want to explicitly return a rejected promise with a value, you should do that with Q.reject.
Calling .done() on promise means that the promise ends there. It cannot be chained further.
So, your code would look like this
exports.update = function (id, template) {
if (!_isValid(template)) {
return Q.reject(new Error('Invalid data', Error.INVALID_DATA));
}
return _update(id, template);
};
Now, the update function just returns a promise always. Its up to the callers to attach the success or failure handlers to it.

Categories

Resources