As others have noted, these calls do not depend on each other, which means you don't need to sequence them and can instead execute them all simultaneously. Here is what that might look like:
module.exports.getGoogleData = function(jwtClient, analytics, VIEW_ID) {
return Promise.all([
getOrdersToday(jwtClient, analytics, VIEW_ID),
getOnlineUsersToday(jwtClient, analytics, VIEW_ID),
getSearchedToday(jwtClient, analytics, VIEW_ID),
getPageviewsTodayAndUsersToday(jwtClient, analytics, VIEW_ID)
])
.then(function(results) {
return {
"Orders": results[0],
"Onlineusers": results[1],
"searched": results[2],
"pageviews": results[3][0].pageviews,
"usersToday": results[3][0].users
};
});
}
But I think your question is a really good one, and I hope your interest in good patterns doesn't end just because this specific case calls for a different solution. As I have noted elsewhere, async patterns is a complicated problem domain.
So, let's pretend you need to make 4 calls, each depending on the results of the previous one, so they must be executed in sequence. We have access to this fictional API:
function fetchOne(null) // returns opaque ONE
function fetchTwo(ONE) // must feed it ONE, returns opaque TWO
function fetchThree(TWO) // must feed it TWO, returns opaque THREE
function fetchFour(THREE) // must feed it THREE, returns opaque FOUR
And furthermore, let's pretend that at the end of this process, you wish to have access to all four of these return values so you can craft a response with all of them. After all, what good is a pattern if it can't accommodate the kinds of complicated workflows we see in the real world?
Thus, our goal is to flesh out the implementation of this function:
function doTheWork() {
// DO STUFF somehow, finally...
return {
one: ONE,
two: TWO,
three: THREE,
four: FOUR,
checksum: ONE + TWO + THREE + FOUR
};
}
A naively straightforward attempt might look like this:
function doTheWork() {
return fetchOne()
.then((ONE) => {
return fetchTwo(ONE)
})
.then((TWO) => {
return fetchThree(TWO)
})
.then((THREE) => {
return fetchFour(THREE)
})
.then((FOUR) => {
// all work is done, let's package up the results
return {
one: ONE // uh-oh...
};
})
}
This will execute, but the problem is that we no longer have access to the earlier values in the final handler.
There are really only two ways around this: (1) declare variables with a scope that will be shared by all the callbacks, or (2) pass all data between handlers in some kind of structure. I think solution 1 is really inelegant, and I recommend against it, but let's see what that might look like:
function doTheWork() {
var A, B, C
return fetchOne()
.then((ONE) => {
A = ONE // store ONE in A
return fetchTwo(ONE)
})
// ...
.then((FOUR) => {
// all work is done, let's package up the results
// at this point, A & B & C contain the results of previous calls
// and are within scope here
return {
one: A,
two: B,
three: C,
four: FOUR,
checksum: A + B + C + FOUR
};
})
}
This will work, but I think it's bad practice and I dislike it. It's bad practice because these variables are now "public" in a limited kind of way, exposing them to everything else within doTheWork
. They can be clobbered by statements anywhere in this function. And unless they are scalars, they will be passed by reference, which means any mutation bugs associated with them can manifest themselves in sometimes bizarre ways.
Also, each of these "temporary" variables now competes for good names in a shared namespace (the scope of doTheWork
). Ideally, you should create as few variables as possible, and each should live only as long as necessary. This might save memory, but it definitely saves your "naming pool." Naming things is mentally exhausting. I am not kidding. And every name must be good -- never name variables in a slapdash way. The names of things are often the only clues within the code about what is happening. If some of those names are throwaway garbage, you are making your life and the lives of future maintainers harder.
So, let's look at solution 2. At this point, I want to point out that this approach works best when you can use ES6 destructuring syntax. You can do this without it, but it's a bit clunkier.
We're going to construct an array that will slowly accumulate all the data fetched by each of these async calls. At the end, the array will look like this:
[ ONE , TWO , THREE , FOUR ]
By chaining promises efficiently, and by passing this array from one handler to the next, we can both avoid the Pyramid of Doom and share async results among all of these methods easily. See below:
function doTheWork() {
return fetchOne()
.then((ONE) => {
return fetchTwo(ONE)
.then((TWO) => [ ONE , TWO ])
})
.then(([ ONE , TWO ]) => {
return fetchThree(TWO)
.then((THREE) => [ ONE , TWO , THREE ])
})
.then(([ ONE , TWO , THREE ]) => {
return fetchFour(THREE)
.then((FOUR) => [ ONE , TWO , THREE , FOUR ])
})
.then(([ ONE , TWO , THREE , FOUR ]) => {
return {
one: ONE,
two: TWO,
three: THREE,
four: FOUR,
checksum: ONE + TWO + THREE + FOUR
}
})
}
That's the whole thing, but let's step through just the first part:
return fetchOne()
.then((ONE) => {
return fetchTwo(ONE)
.then((TWO) => [ ONE , TWO ])
})
As usual, fetchOne
returns ONE
-- it's the third-party API we have no control over. And as you might expect, we use ONE
to make the second call, making sure to return
its promise. But that last line is the real magic:
.then((TWO) => [ ONE , TWO ])
The second API call still returns just TWO
, but rather than us simply returning TWO
alone, we instead return an array that contains both ONE
and TWO
. That value -- the array -- becomes the argument to the next .then
handler.
This works because nested promises are automatically unwrapped. Here's a simple example showing that at work:
function getDogName() {
return fetchCatName()
.then((catName) => 'Fido')
}
getDogName()
.then((dogName) => {
console.log(dogName);
})
// logs 'Fido'
This illustrates that you can attach .then
handlers to nested promises, and the outer promises will return the results of those handlers. This is a really common pattern when using fetch
to get JSON:
function makeApiCall() {
fetch(api_url) // resolves with a Response object
.then((response) => response.json()) // extracts just the JSON and returns that instead!
}
Going back to our code, since we want to avoid the Pyramid, we leave this handler along (rather than nesting the next call inside, as you originally did). The next line looks like this:
.then(([ ONE , TWO ]) => {
This is the ES6 destructuring at work. By destructuring the array argument, we can name the elements as they enter the function. And if we're doing that, we might as well give them the names we like best, the ones the came into the world as: ONE
and TWO
.
We then repeat the pattern, using TWO
to invoke fetchThree
, making sure to return its promise, but not before tacking on a tiny .then
handler that bundles the newly-available THREE
into the package that we pass forward.
I hope this helps. It's a pattern I worked out to deal with some complicated branching workflows in AWS, making calls against S3 and Dynamo and other platforms with a lot of parallel conditionals, mixing blocking and non-blocking behavior.
Async patterns are a special problem domain. Even with the right tech, it's can be hard to find clear ways to express behavior, especially when the underlying workflow is convoluted. This is often the case when exploring the data graphs that are so common these days.
Happy coding.