Side Effects in JS Promise chains
Here I’m presenting two cool little functions that are making my days spent in the JS Promise land just a bit happier.
All code snippets are written using ECMAScript 2015.
So, I’ve often found myself writing code like this:
const getUser = (id) => {
return httpClient.get('https://facebook.com/user/' + id)
.then((response) => {
// Side Effects Logic
return cacheData(response);
})
.then((response) => parseUserResponse(response))
.then((user) => {
// more side effects on the User Data now updateLocalDatabase(user);
return user;
})
then((user => {
// even more side effects refreshUserCreditCards(user);
return user;
});
}
const cacheData = (response) => {
cacheStore.put('user', response); return response;
}
This is not ideal because it forces me to either burden the side-effect handling methods with the knowledge of what to return (see cacheData()), thus combining a command function with a query function OR to wrap the side-effect methods in an anonymous function that returns the passed argument. The former can open a pretty big can of worms if not carefully dealt with while the latter is just cumbersome.
What if i could write code like this:
const getUser = (id) => {
return httpClient.get('https://facebook.com/user/' + id)
.then(cacheData)
.then(parseUserResponse)
.then(updateLocalDatabase)
.then(refreshUserCreditCards);
}
Right now, by calling getUser() I would get undefined, because the last side-effect handler doesn’t return anything, like:
const getUser = (id) => {
return httpClient.get('https://facebook.com/user/' + id)
.then(cacheData) // => response
.then(parseUserResponse) // => user
.then(updateLocalDatabase) // => undefined
.then(refreshUserCreditCards); // => undefined
}getUser(45)
.then(user => {
console.log(user); // output: undefined
});
Enter passThrough():
const getUser = (id) => {
return httpClient.get('https://facebook.com/user/' + id)
.then(passThrough(cacheData)) // => response
.then(parseUserResponse) // => user
.then(passThrough(updateLocalDatabase)) // => user
.then(passThrough(refreshUserCreditCards)); // => user
}getUser(45)
.then(user => {
console.log(user); // output: {name: Frank, age: 23, id: 45 }
});
This little function ensures that data flows correctly thru the promise chain, while it abstracts the logic away from the implementation. In fact the logic is stolen from that cumbersome wrapper in the example above, and it looks like this:
const passThrough = (fn) => (a) => {
fn(a); // process side-effects
return a; // pass the data further
};
Pretty simple, right?
That’s cool, but what if the side-effects are async, and I need to make sure they finished before continuing? Or what if I don’t want to handle a side-effect, but just to stop the current chain, wait for a particular event to resolve, and continue from where I left off?
Enter passThroughAwait()
const purchase = (items) => {
return Promise.resolve()
.then(showConfirmDialog) // => Promise that waits for User input
.then(() => createOrder(items)) // => Order Payload
.then(passThrough(trackAnalytics)) // => Order Payload
.then(passThroughAwait(showSuccessDialog)) // => Order Payload
.then(passThroughAwait(askForReviewDialog)) // => Order Payload
.then(passThrough(postPurchaseCleanUp)); // => Order Payload
}
And this is how the chained methods look like:
const showConfirmDialog = () => {
return dialog.show(msgs.confirmPurchase); // => Promise
}const createOrder = (items) => {
return httpClient.post('/api/orders', {items});
}const trackAnalytics = (order) => {
analytics.triggerEvent('purchase', {
total: order.total,
...
});
}const showSuccessDialog = () => {
return dialog.show(msgs.successPurchase); // Promise
}const askForReview = () => {
return dialog.confirm(msg.reviewPurchase); // => Promise
}const postPurchaseCleanup = () => {
// invalidate caches
// remove templates
// close DB connection
// etc... // returns undefined
}
passThroughAwait() works exactly like passThrough() with the only difference that it waits for the side-effect handler to resolve, before continuing. The only note here, is that all the side-effect handlers must return a Promise, which is mandatory regardless passThroughAwait() is used or not.
I mostly use it when handling actions triggered by the user, such as purchasing an order, because I can easily start a Promise chain, “pause” it to show a confirmation dialog, wait for the user to accept/deny, grab the order information passed through and create the server request, show a success/error notification, track analytics and perform some cleanup action at the end, while keeping the code super terse and easy to follow.
But what I like the most about it is that both passThrough and passThroughAwait clearly separate side-effect handling functions (commands) from pure functions (queries), thus leaving the code a bit cleaner, readable and easier to reason about.
Thanks for reading. Feedback is greatly appreciated!
Update: December 11, 2017
It took me a while, but you can now find on npm (https://www.npmjs.com/package/promise-passthrough):
$ npm install promise-passthrough