async function doStuff() {
try {
// Fetch things concurrently
const [
foos,
bars,
bazs,
] = await Promise.all([
getFoos().then((res) => {
console.log(`getFoos done with ${res}`
return res
})),
getBars(),
getBazs()
.catch((err) => {
// handle only the error in getBazs
}),
]);
// Do stuff with them...
} catch (err) {
// handle any errors
}
}
it still isn't what i'd call "pretty" code, but it's simpler for me to read and comprehend than doing most of those things without async/await or Promise.all or other helpers like that.As always, who you are working with matters a LOT, and if I were on a team where a good portion of the devs weren't comfortable with that style of code, then I'd use more verbose ways of doing it that are more inline with what they are expecting.
But on the rare occasion that the complexity necessitates the advanced promise stuff, or in jobs (like my current one) where all of us devs are super comfortable with promises being used like this, then it's a dream to work with them!
My Promise.all example will fire off the getFoos, getBars, and getBazs requests all at the same time, and then do things at different moments with the results in some cases. Yours will do them serially like this:
getFoos -> wait for it to resolve -> run getBars -> wait for it to resolve -> run getBazs -> wait for it to resolve -> run doStuff
And if individually handled errors is what you are after ala go, then this also does it while preserving the parallelism:
async function doStuff() {
// Fetch things concurrently
const [
foos,
bars,
bazs,
] = await Promise.all([
getFoos().catch((err) => {
// handle only the error in getFoos
}),
getBars().catch((err) => {
// handle only the error in getBars
}),
getBazs().catch((err) => {
// handle only the error in getBazs
}),
]);
// Do stuff with them...
}
And I'm not suggesting we always use promises in every area, but that for some things they are the perfect tool for the job. There are some things that promises are really bad at that callbacks do really well. One example is progress systems.A callback can be called multiple times over the course of an async call with percentage it's completed every call. A promise is one-and-done, and that's a big issue in many areas. But that's why callbacks aren't really "deprecated" as much as they are relegated to doing what they do best, while leaving things that Promises and async/await do best to them.
I would do it like this (if I didn't have access to "async/await" which is sadly the case for me in Node sometimes):
function doStuff (foos, bars, bazs, callback) {
// do stuff
}
function getStuff (callback) {
let foos, bars, bazs;
let semaphore = 3;
function collector () {
if (--semaphore) {
return;
}
doStuff(foo, bars, bazs, callback);
}
getFoos((err, res) => {
if (err) {
return callback(err);
}
foos = res;
collector();
});
getBars((err, res) => {
if (err) {
return callback(err);
}
bars = res;
collector();
});
getBazs((err, res) => {
if (err) {
return callback(err);
}
bazs = res;
collector();
});
}
It is more lines of code but is boringly simple to read and allows for fully custom error handling (an example of the semaphore + collector pattern I mentioned below in another comment).The custom error handling would be in the individual "get" functions which allows for easier testing.
The semaphore in the collector is the most tricky part and I would possibly replace it here with an "if" to check that all the variables have a value - depends on what sort of data I could get back.
Promises are nice because they have a single purpose, to resolve or reject a promised value. So when I see them, I can pretty much instantly understand what it's doing. `Promise.all` tells me that it's waiting for all of them to resolve at once, `Promise.race` tells me that the result will be the first of them to resolve or reject.
With callbacks, everything is "custom". So looking at your example I'd need to fully read the function to get an understanding of what it's doing, vs just knowing that Promise.all means "wait for them all and then resolve".
Granted, if where you are working tends to avoid promises or heavily uses a language like golang where your style is much more common, then I actually completely agree with you that the callback version is going to be more maintainable, readable, and easier to work with in your team.
But if/when your org/team collectively learns the "shorthand" that promises can provide, IMO they become much simpler to read and understand in most cases. It's basically a way of providing structure to really common patterns like yours, so that the actual structure fades into the background and the important stuff (what you are trying to do) becomes the only focus.
In other words it's syntactic sugar. With all the benefits and drawbacks that any syntactic sugar provides.
The little bit of magic that makes your code sane is the "await" in front of "Promise.all()" and makes it more elegant than the callback version.
I'd actually say that it's the `Promise.all` that makes it sane, the await could be replaced by a `.then` and be functionally the same.
Actually it might be even easier to read to someone who doesn't live and breathe javascript as it will keep the program flowing top-to-bottom:
Promise.all([a(), b(), c()]).then(([a, b, c]) => {
// do stuff...
})
As with most things javascript, there are 9 different ways of doing it, and 3 of them will shoot you in the face...
With the exception of necessarily serial code that I referred to, this statement is not true in my experience. Your "legible" example is only legible because you are not doing anything actually important like error handling and logging. In the real world each call may need different logging or even error handling logic and that is when promises really turn into a rats nest.
Validation, i.e. tests, are really the same regardless which approach you are using.
I do agree that async/await made promises actually usable, as I said above.
For running a bunch of asynchronous functions that do the same thing the semaphore + collector pattern works just fine and is not much more verbose.