Async await orchestration (Parallel, Sequential, Timeouts)
Why Orchestration, Here we will be looking at structuring how multiple asynchronous tasks interact, in order to produce one coherent result. Just like in an orchestra:
- Each musician = an async task.
- Some play solos first (sequential).
- Some play together (parallel). It's up to us to decide the timing, the order, and what happens if one instrument fails. If we don't have orchestration, we just have “random musicians playing.” With orchestration, we have a symphony. Here we will be using a user profile example as our 'orchestra'. 🟠
Async/await is very powerful
async/await in JavaScript is nice, just write await in front of a promise and no more .then() chains but wait ... after knowing the syntax and you are solving real world problems questions start arising; when do I run tasks sequentially(one after the other), when do I run them in parallel(at the same time), and how do I guard against timeouts.
To orchestrate async well we need to ask ourselves 4 questions
1. Does one task depend on the result of the previous?
- In sequential every call must wait for the previous result.jsHere fetching posts depends on fetching the user thus sequential is the right choice.
const user = await fakeApi("/user", { id: 101 }); const posts = await fakeApi("/posts", { userId: user.data.id }); - In parallel calls are independent, run them at the same time.js
const [comments, likes] = await Promise.all([ fakeApi("/comments", [`c1`, `c2`]), fakeApi("/likes", 42) ]); ``` Comments don’t depend on likes. Running them sequentially will waste time.
2. What happens if a task hangs forever?
Imagine you are making an API call, issues may arise on the server or even networks may fail, therefore we will need to add timeout guard so that the system does not stall.
function withTimeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`⏰ Timeout after ${ms}ms`)), ms)
)
]);
}Above Promise.race([...]), runs multiple promises and settles with whichever finishes first (resolve or reject) with one competitor being your promise (the async task we actually care about) and the other competitor is a timer that rejects after ms milliseconds. Thus if the async task finishes before the timeout we get its result otherwise if it takes too long we get a timeout error.
Now we can add timeout in our async operations
const user = await withTimeout(fakeApi("/user", { id: 101 }, 800), 2000);3. Can I collapse multiple async operations into one pass?
From our #1 we see likes and comments for a posts are independent in that one does not wait for the other to finish executing so that it executes thus instead of waiting twice lets say fetching comments takes 3000ms and likes takes 5000ms sequentially we get 8000ms, but if we do everything in one go we get max(5000, 3000) which gives 5000 saving 3000ms.
- Here we can make use of Promise.all
const postsWithCommentsAndLikes = await Promise.all(
posts.data.map(async (post) => {
const [comments, likes] = await Promise.all([
fakeApi("/comments", [`c1`, `c2`], 600),
fakeApi("/likes", Math.floor(Math.random() * 100), 400)
]);
return { ...post, comments: comments.data, likes: likes.data };
})
);In the code above;
- posts.data.map(async (post) => { ... }) Iterates over every post in posts.data. For each post, it runs an async function that will add more info (likes, comments). The result of .map() is an array of promises (because of async).
- Promise.all([ fakeApi(...), fakeApi(...) ]) This helps us to do two things at the same time: that is Fetch comments for the post, simulated by fakeApi("/comments", ...) and fetch likes for the post, simulated by fakeApi("/likes", ...). Promise.all runs both in parallel and waits until both finish with result being an array [commentsResult, likesResult].
- return { ...post, comments: comments.data, likes: likes.data } Once both API calls resolve, we create a new post object: where we spread original post ({ ...post }), add a comments property from the comments.data and add a likes property from the likes.data.
note
using the above example if any comments or likes request fails, the entire batch rejects meaning all posts with comments and likes are lost, even if others succeeded. This approach would be useful if you need all-or-nothing consistency for example if data is incomplete, don’t render it.
4. But what if need comments and posts to fetch at the same time and display one even if the other fail?
This is the go to solution for resilience in unreliable networks and API endpoints. So we will require each task to report whether it failed or if it succeeded and handle the error( here we do empty array for comments and 0 for likes).
const postsWithCommentsAndLikes = await Promise.all(
posts.data.map(async (post) => {
const [comments, likes] = await Promise.allSettled([
fakeApi("/comments", [`c1`, `c2`], 600),
fakeApi("/likes", Math.floor(Math.random() * 100), 400)
]);
return {
...post,
comments: comments.status === "fulfilled" ? comments.value.data : [],
likes: likes.status === "fulfilled" ? likes.value.data : 0,
};
})
);Our failure strategy
From the above we can say
Strategy
Promise.all - fails fast if any promise rejects. Great when all or nothing makes sense. Promise.allSettled - waits for all results, even if some fail. Perfect when partial results are still valuable.
Perfomance review of sequential vs parallel
- Sequential (one after the other): If Post 1 comments takes 600ms and Post 1 likes takes 1000ms, then Post 2 comments takes 1600ms, total time = 2000ms.
- Parallel (all at once): When using
Promise.all, the overall time is bounded by the slowest operation in the batch. For example, if comments take 600ms and likes take 1000ms, then both together finish in ~1000ms, not 1600ms. - The timing numbers above are just examples to show how much time you save; in practice, the actual duration = the longest-running task.
Putting it all together
// Utility: adds a timeout guard around any async task
function withTimeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`⏰ Timeout after ${ms}ms`)), ms)
)
]);
}
// Simulated async services with artificial latency
function fakeApi(endpoint, data, delay = 1000) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ endpoint, data, delay });
}, delay);
});
}
// Main orchestrator
async function fetchUserProfile(userId) {
try {
// Step 1: get user
const user = await withTimeout(
fakeApi("/user", { id: userId, name: "Koome" }, 800),
2000
);
console.log(" User:", user);
// Step 2: get posts for user (sequential, depends on user)
const posts = await withTimeout(
fakeApi("/posts", [{ id: 1 }, { id: 2 }], 1200),
2000
);
console.log("Posts:", posts);
// Step 3: fetch comments + likes for each post (parallel, resilient)
const postsWithCommentsAndLikes = await Promise.all(
posts.data.map(async (post) => {
const [comments, likes] = await Promise.allSettled([
fakeApi("/comments", [`c1`, `c2`], 600),
fakeApi("/likes", Math.floor(Math.random() * 100), 400)
]);
return {
...post,
comments: comments.status === "fulfilled" ? comments.value.data : [],
likes: likes.status === "fulfilled" ? likes.value.data : 0,
};
})
);
console.log(" Posts with comments and likes:", postsWithCommentsAndLikes);
return { ...user.data, posts: postsWithCommentsAndLikes };
} catch (err) {
console.error(" Error fetching profile:", err.message);
return null;
}
}
// Run
fetchUserProfile(101);To review; the above code simulates fetching a user profile with async orchestration: it first gets the user (/user) and then their posts (/posts) in sequence since each depends on the previous step, and finally enriches each post in parallel by fetching comments and likes using Promise.allSettled so failures don’t cancel everything. Each post ends up with comments (or an empty array if the request failed) and likes (or 0 on failure), stored in postsWithCommentsAndLikes. The function returns the user object merged with the enriched posts, while withTimeout ensures no request hangs forever by rejecting if it exceeds a time limit.
javascript book
If this interested you, check out my Javascript Book