How can I avoid promise chain drilling? - javascript

My promise chain looks like this:
PromiseA()
.then((A) => PromiseB(A))
.then((B) => PromiseC(B))
...
.then((X) => PromiseY(X))
.then((Y) => PromiseZ(Y, A))
How do I use parameter A in the last promise without drilling through all the promises like so:
PromiseA()
.then((A) => Promise.all[A, PromiseB(A)])
.then(([A, B]) => Promise.all[A, PromiseC(B)])
...
.then(([A, X]) => Promise.all[A, PromiseY(X)])
.then(([A, Y]) => PromiseZ(A, Y))

Refactor your function to async/await:
async function fun() {
const A = await PromiseA();
const B = await PromiseB(A);
const C = await PromiseC(B);
// ...
const Y = await PromiseY(A, B, C);
const Z = await PromiseZ(Y, A);
}

You can nest the chain inside the first .then() callback so that A is still in scope.
PromiseA()
.then((A) =>
PromiseB(A)
.then(PromiseC)
.then(PromiseD)
.then(PromiseE)
...
.then((Y) => PromiseZ(Y, A)
);

You can always use a variable to store it.
let a;
PromiseA()
.then(A => (a = A, PromiseB(A))
.then(B => PromiseC(B))
...
.then(X => PromiseY(X))
.then(Y) => PromiseZ(a, Y));

Related

JavaScript: how can I tweak this function to return all the results from the async functions in order

I have a function that takes an array of async/sync functions and invoke each of them sequentially (as opposed to in parallel) in the same order as the input is passed.
For example:
const sleep = (delay) => new Promise((r) => setTimeout(r, delay))
const fn1 = async () => {
await sleep(2000)
console.log('fn1')
return 'fn1'
}
const fn2 = async () => {
await sleep(3000)
console.log('fn2')
return 'fn2'
}
const fn3 = async () => {
await sleep(1000)
console.log('fn3')
return 'fn3'
}
const fn4 = () => {
console.log('fn4')
return 'fn4'
}
function serializeAsyncFns(fns) {
return fns.reduce(
(promise, fn) => promise.then(() => fn()),
Promise.resolve()
)
}
serializeAsyncFns([fn1, fn2, fn3, fn4])
// fn1 -> fn2 -> f3 -> f4
But now the return value of serializeAsyncFns is a promise that resolves to the return value of the last function in the input list, which is f4. Is there a way to tweak this funciton so that the returned promise resolves to the an array of values of all the functions, in order of how they got passed.
In this case it would be ['fn1', 'fn2', 'fn3', 'fn4']
Promise.all doesn't work here as it would fire all the promises in parallel.
easiest way is with a for loop and async/await
async function serializeAsyncFns(fns) {
const result = [];
for (const fn of fns) {
result.push(await fn());
}
return result;
}
If, for some reason you can't use async/await for that function, this is what I used to do before async/await was a thing
const serializeAsyncFns = fns =>
fns.reduce((promise, fn) =>
promise.then(results => Promise.resolve(fn()).then(result => [...results, result])),
Promise.resolve([])
);

JavaScript - Promise.allSettled + Array.reduce()

Introduction
Imagine this method for getting the language of a user:
const getUserLanguage = (userId) => new Promise(
(resolve, reject) => {
if (Math.random() < 0.3) resolve("en");
if (Math.random() < 0.6) resolve("es");
reject("Unexpected error.");
}
);
(async () => {
try {
const language = await getUserLanguage("Mike")
console.log(`Language: ${language}`);
} catch(err) {
console.error(err);
}
})();
Now, I am trying to group the language of multiple users, performing a parallel request:
const getUserLanguage = () => new Promise(
(resolve, reject) => {
if (Math.random() < 0.3) resolve("en");
if (Math.random() < 0.6) resolve("es");
reject("Unexpected error.");
}
);
const groupUsersByLanguage = async (userIds) => {
const promiseResults = await Promise.allSettled(
userIds.reduce(async (acc, userId) => {
const language = await getUserLanguage(userId);
(acc[language] = acc[language] ?? []).push(userId);
return acc;
}, {})
);
console.log({ promiseResults });
// Filter fulfilled promises
const result = promiseResults
.filter(({ status }) => status === "fulfilled")
.map(({ value }) => value);
return result;
}
(async () => {
const userIds = ["Mike", "Walter", "Saul", "Pinkman"];
const usersGroupedByLanguage = await groupUsersByLanguage(userIds);
console.log(usersGroupedByLanguage);
})();
Problem
But my implementation is not working:
const promiseResults = await Promise.allSettled(
userIds.reduce(async (acc, userId) => {
const language = await getUserLanguage(userId);
(acc[language] = acc[language] ?? []).push(userId);
return acc;
}, {})
);
How can I do for getting an output like
{
"es": ["Mike", "Saul"],
"en": ["Walter"],
}
using the Promise.allSettled combined with .reduce?
Your .reduce is constructing an object where each value is a Promise. Such an object is not something that .allSettled can understand - you must pass it an array.
I'd create an object outside, which gets mutated inside a .map callback. This way, you'll have an array of Promises that .allSettled can work with, and also have the object in the desired shape.
const getLanguage = () => new Promise(
(resolve, reject) => {
if (Math.random() < 0.3) resolve("en");
if (Math.random() < 0.6) resolve("es");
reject("Unexpected error.");
}
);
const groupUsersByLanguage = async (userIds) => {
const grouped = {};
await Promise.allSettled(
userIds.map(async (userId) => {
const language = await getLanguage(userId);
(grouped[language] = grouped[language] ?? []).push(userId);
})
);
return grouped;
}
(async () => {
const userIds = ["Mike", "Walter", "Saul", "Pinkman"];
const usersGroupedByLanguage = await groupUsersByLanguage(userIds);
console.log(usersGroupedByLanguage);
})();
An option that doesn't rely on side-effects inside a .map would be to instead return both the userId and the language inside the map callback, then filter the allSettled results to include only the good ones, then turn it into an object.
const getLanguage = () => new Promise(
(resolve, reject) => {
if (Math.random() < 0.3) resolve("en");
if (Math.random() < 0.6) resolve("es");
reject("Unexpected error.");
}
);
const groupUsersByLanguage = async (userIds) => {
const settledResults = await Promise.allSettled(
userIds.map(async (userId) => {
const language = await getLanguage(userId);
return [userId, language];
})
);
const grouped = {};
settledResults
.filter(result => result.status === 'fulfilled')
.map(result => result.value)
.forEach(([userId, language]) => {
(grouped[language] = grouped[language] ?? []).push(userId);
});
return grouped;
}
(async () => {
const userIds = ["Mike", "Walter", "Saul", "Pinkman"];
const usersGroupedByLanguage = await groupUsersByLanguage(userIds);
console.log(usersGroupedByLanguage);
})();
I would write a main function using two utility functions for this: one that groups a set of elements according to the result of a function, and one that takes a predicate function and partitions an array into those ones for which it returns true and those ones for which it returns false. These two in turn use a push utility function which simply reifies Array.prototype.push into a plain function.
The main function maps the getUserLanguage function over the users, calls Promise.allSettled on the results, then we map over the resulting promises, to connect the original userId back with the promise results. (If the fake getUserLanguage returned an object with properties for both the userId and language, this step would be unnecessary.) Then we partition the resulting promises to separate out the fulfilled from the rejected ones. I do this because your question doesn't say what to do with the rejected language lookups. I choose to add one more entry to the output. Here as well as es and en, we also get a list of userIds under _errors. If we wanted to ignore these, then we could replace the partition with a filter and simplify the last step. That last step takes successful results and the failures, combining the successful ones into an object with our group helper, and appending the _errors, by mapping the failures to their userIds.
It might look like this:
// dummy implementation, resolving to random language, or rejecting with error
const getUserLanguage = (userId) => new Promise ((resolve, reject) => {if (Math.random() < 0.3) resolve("en"); if (Math.random() < 0.6) resolve("es"); reject("Unexpected error.");});
// utility functions
const push = (x) => (xs) =>
(xs .push (x), xs)
const partition = (fn) => (xs) =>
xs .reduce (([y, n], x) => fn (x) ? [push (x) (y), n] : [y, push (x) (n)], [[], []])
const group = (getKey, getValue) => (xs) =>
xs .reduce ((a, x, _, __, key = getKey (x)) => ((a [key] = push (getValue (x)) (a[key] ?? [])), a), {})
// main function
const groupUsersByLanguage = (users) => Promise .allSettled (users .map (getUserLanguage))
.then (ps => ps .map ((p, i) => ({...p, user: users [i]})))
.then (partition (p => p .status == 'fulfilled'))
.then (([fulfilled, rejected]) => ({
...group (x => x .value, x => x.user) (fulfilled),
_errors: rejected .map (r => r .user)
}))
// sample data
const users = ['fred', 'wilma', 'betty', 'barney', 'pebbles', 'bambam', 'yogi', 'booboo']
// demo
groupUsersByLanguage (users)
.then (console .log)
.as-console-wrapper {max-height: 100% !important; top: 0}
This yields output like this (YMMV because of the random calls):
{
en: [
"fred",
"wilma",
"barney"
],
es: [
"bambam",
"yogi",
"booboo"
],
_errors: [
"betty",
"pebbles"
]
}
Note that those utility functions are general-purpose. If we keep our own libraries of such tools handy, we can write functions like this without great effort.
Another option of doing this would be to first fetch all languages using:
const languages = await Promise.allSettled(userIds.map(getLanguage));
Then zip then together with userIds and process them further.
async function getLanguage() {
if (Math.random() < 0.3) return "en";
if (Math.random() < 0.6) return "es";
throw "Unexpected error.";
}
function zip(...arrays) {
if (!arrays[0]) return;
return arrays[0].map((_, i) => arrays.map(array => array[i]));
}
async function groupUsersByLanguage(userIds) {
const languages = await Promise.allSettled(userIds.map(getLanguage));
const groups = {};
for (const [userId, language] of zip(userIds, languages)) {
if (language.status != "fulfilled") continue;
groups[language.value] ||= [];
groups[language.value].push(userId);
}
return groups;
}
(async () => {
const userIds = ["Mike", "Walter", "Saul", "Pinkman"];
const usersGroupedByLanguage = await groupUsersByLanguage(userIds);
console.log(usersGroupedByLanguage);
})();
If you are not interested in creating a zip() helper you can use a "normal" for-loop:
const groups = {};
for (let i = 0; i < userIds.length; i += 1) {
if (languages[i].status != "fulfilled") continue;
groups[languages[i].value] ||= [];
groups[languages[i].value].push(userId);
}

Chain async fallbacks and print their errors only if all fail

I want to invoke an async method, followed by a series of async fallback methods until one of them succeeds.
If all invocations fail, then I want all of their errors printed. Otherwise, if even one succeeds, the errors should not be printed.
This is what I want:
tryX()
.catch(x => tryXFallback1()
.catch(xf1 => tryXFallback2()
.catch(xf2 => tryXFallback3()
.catch(xf3 => tryXFallback4()
// ...
.catch(xf4 => Promise.reject([x, xf1, xf2, xf3, xf4]))))));
But I'm not a fan of the indentation. Accumulating the errors in a variable outside the scope of the catch clauses also seems messy:
let errors = [];
tryX()
.catch(x => {
errors.push(x);
return tryXFallback1();
})
.catch(xf1 => {
errors.push(x);
return tryXFallback2();
})
.catch(xf2 => {
errors.push(x);
return tryXFallback3();
})
.catch(xf3 => {
errors.push(x);
return tryXFallback4();
})
// ...
.catch(xf4 => Promise.reject(errors));
Lastly, I thought I could do some sort of for loop instead but that seems even uglier e.g.:
let methods = [tryX, tryFallback1, tryFallback2, tryFallback3, tryFallback4, /*...*/];
let errors = [];
for (let x of methods)
try {
return await x();
} catch (e) {
errors.push(e);
}
if (errors.length === methods.length)
return Promise.reject(errors);
Does anyone know of a more elegant approach?
The loop you have seems fine. I would probably stick with it as it already works. However, here is an alternative:
function tryWithFallbacks(main, ...fallbacks) {
return fallbacks.reduce(
(p, nextFallback) => p.catch( //handle errors
err => nextFallback() //try using the fallback
.catch(e => Promise.reject(err.concat(e))) //propagate rejection reasons
//on failure by adding to
//the array of errors
),
main() //seed the process with the main task
.catch(err => Promise.reject([err])) //ensure there is an array of errors
);
}
const a = tryWithFallbacks(
() => Promise.resolve(42)
);
test(a, "a"); //42
const b = tryWithFallbacks(
() => Promise.reject("oops"),
() => Promise.resolve("it's fine")
);
test(b, "b"); //"it's fine"
const c = tryWithFallbacks(
() => Promise.reject("oops1"),
() => Promise.reject("oops2"),
() => Promise.reject("oops3")
);
test(c, "c"); //["oops1", "oops2", "oops3"]
const d = tryWithFallbacks(
() => Promise.reject("oops1"),
() => Promise.reject("oops2"),
() => Promise.reject("oops3"),
() => Promise.resolve("finally!")
);
test(d, "d"); //"finally!"
const e = tryWithFallbacks(
() => Promise.reject("oops1"),
() => Promise.reject("oops2"),
() => Promise.reject("oops3"),
() => Promise.resolve("penultimate try successful!"),
() => Promise.reject("this is not reached")
);
test(e, "e"); //"penultimate try successful!"
//a simple function to illustrate the result
function test(promise, name) {
promise
.then(result => console.log(`[${name}] completed:`, result))
.catch(errorResult => console.log(`[${name}] failed:`, errorResult));
}
It's Array#reduce()-ing all the promises into one and making sure of the sequential order. If any succeed, you just get a single successful result. On failure, the error response is added to an array of all errors and passed forward via the rejection flow of promises.
This currently does require that all the functions that produce a promise are thunks - they take no input.
For reference, an equivalent operation using await and a loop would be:
async function tryWithFallbacks(...tasks) {
const errors = [];
for (const task of tasks) {
try {
const result = await task();
return result;
} catch (err) {
errors.push(err);
}
}
return errors;
}
const a = tryWithFallbacks(
() => Promise.resolve(42)
);
test(a, "a"); //42
const b = tryWithFallbacks(
() => Promise.reject("oops"),
() => Promise.resolve("it's fine")
);
test(b, "b"); //"it's fine"
const c = tryWithFallbacks(
() => Promise.reject("oops1"),
() => Promise.reject("oops2"),
() => Promise.reject("oops3")
);
test(c, "c"); //["oops1", "oops2", "oops3"]
const d = tryWithFallbacks(
() => Promise.reject("oops1"),
() => Promise.reject("oops2"),
() => Promise.reject("oops3"),
() => Promise.resolve("finally!")
);
test(d, "d"); //"finally!"
const e = tryWithFallbacks(
() => Promise.reject("oops1"),
() => Promise.reject("oops2"),
() => Promise.reject("oops3"),
() => Promise.resolve("penultimate try successful!"),
() => Promise.reject("this is not reached")
);
test(e, "e"); //"penultimate try successful!"
//a simple function to illustrate the result
function test(promise, name) {
promise
.then(result => console.log(`[${name}] completed:`, result))
.catch(errorResult => console.log(`[${name}] failed:`, errorResult));
}
Use Promise.any():
Promise.any([tryXFallback1, tryXFallback2, tryXFallback3]).then(result => console.log(result)).catch(err => console.log(err.errors))
This solves everything you need:
Works if any async of the passed array is resolved
Logs all of the errors that were thrown (The object that is passed to the catch callback of any, has property .errors)

How to transform an imperative Promise to a functional Task?

I want to transform an imperative Promise to a functional Task in a principled fashion:
const record = (type, o) =>
(o[type.name || type] = type.name || type, o);
const thisify = f => f({});
const taskFromPromise = p =>
Task((res, rej) =>
p.then(res)
.catch(x => rej(`Error: ${x}`)));
const Task = task => record(
Task,
thisify(o => {
o.task = (res, rej) =>
task(x => {
o.task = k => k(x);
return res(x);
}, rej);
return o;
}));
const tx = taskFromPromise(Promise.resolve(123)),
ty = taskFromPromise(Promise.reject("reason").catch(x => x));
// ^^^^^ necessary to avoid uncaught error
tx.task(console.log); // 123
ty.task(console.log); // "reason" but should be "Error: reason"
The resolution case works but the rejection doesn't, because Promises are eagerly triggered. If I dropped the catch handler I would have to put the entire computation into a try/catch statement. Is there a more viable alternative?
You don't need .catch(x => x), which turns your rejected promise into an resolved one. You shouldn't get an "uncaught error" since you have a catch inside your taskFromPromise function. Removing the .catch(x => x) does work and lands you in .catch(x => rej(`Error: ${x}`)) instead.
However removing .catch(x => x) does throw "TypeError: rej is not a function". From your code sample it it seems like your .task(...) (in tx.task(...) and ty.task(...)) expects two functions. The first one is called if the promise resolves, the second one if the promise is rejected. Providing both functions leaves me with a working code snippet.
const record = (type, o) =>
(o[type.name || type] = type.name || type, o);
const thisify = f => f({});
const taskFromPromise = p =>
Task((res, rej) =>
p.then(res)
.catch(x => rej(`Error: ${x}`)));
const Task = task => record(
Task,
thisify(o => {
o.task = (res, rej) => // expects two functions
task(x => {
o.task = k => k(x);
return res(x);
}, rej);
return o;
}));
const tx = taskFromPromise(Promise.resolve(123)),
ty = taskFromPromise(Promise.reject("reason")); // removed .catch(x => x)
tx.task(console.log, console.log); // provide two functions
ty.task(console.log, console.log);
// ^ ^
// if resolved if rejected

Cleaner Promise.all syntax?

I am fairly new to Node.JS, and I really hate the syntax of Promise.all returning an array.
eg.
const requiredData = await Promise.all([
getFirst(city),
getSecond(hubIds),
getThird(city, customerCategoryKey),
getFourth(request)
])
const firstData = requiredData[0];
const secondData = requiredData[1];
const thirdData = requiredData[2];
const fourthData = requiredData[3];
I need to individually fetch them in separate lines of code.
Isn't there a way like
const {
firstData,
secondData,
thirdData,
fourthData
} = await Promise.all([
getFirst(city),
getSecond(hubIds),
getThird(city, customerCategoryKey),
getFourth(request)
])
Basically, I'd really like if there is a cleaner way than the first code snippet.
TIA!
As mentioned in the comments you can use Array destructor instead of using Object destructor:
(async () => {
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "foo");
});
// get all elements as variables
const [p1, p2, p3] = await Promise.all([promise1, promise2, promise3]);
console.log(p1, p2, p3);
})();
In case it's not obvious, if you're okay running the promises in serial order, you can await them inline -
const main = async () =>
{ const a = await mock("one")
const b = await mock("two")
const c = await mock("three")
const d = await mock("four")
console.log(a, b, c, d)
}
// test funcs
const rand = n =>
Math.floor(Math.random() * n)
const mock = (x) =>
new Promise(r => setTimeout(r, rand(1000), x))
// run
console.log("loading...")
main().catch(console.error)
// loading...
// one two three four
If you want to run the promises in parallel but retrieve assign the values by name, we could fromDescriptor which does the wiring for us -
const fromDescriptor = (desc = {}) =>
Promise.all(
Object
.entries(desc)
.map(([ k, px ]) => px.then(x => [ k, x ]))
)
.then(Object.fromEntries)
const main = async () =>
{ const init =
{ a: mock("one")
, b: mock("two")
, c: mock("three")
, d: mock("four")
}
const { a, b, c, d } =
await fromDescriptor(init)
console.log(a, b, c, d)
}
// test funcs
const rand = n =>
Math.floor(Math.random() * n)
const mock = (x) =>
new Promise(r => setTimeout(r, rand(1000), x))
// run
console.log("loading...")
main().catch(console.error)
// loading...
// one two three four

Categories

Resources