JavaScript
Async Programming
Promises
Asynchronous Code
Concurrency

Waiting until two async blocks are executed before starting another block

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In JavaScript, a frequent requirement is to wait for two independent asynchronous operations before starting a third one. The reliable solution is to compose promises explicitly rather than coordinating with shared flags or timing assumptions. When this pattern is done well, code becomes easier to reason about and failures are handled in one place.

Use Promise Composition Instead Of Manual State

The most direct pattern is Promise.all. It starts all input promises, waits until both resolve, and then passes their results to the next step. If any promise rejects, the combined promise rejects immediately.

javascript
1function fetchUser(userId) {
2  return Promise.resolve({ id: userId, name: 'Mina' });
3}
4
5function fetchPermissions(userId) {
6  return Promise.resolve(['read', 'write']);
7}
8
9function buildSession(user, permissions) {
10  return { userId: user.id, name: user.name, permissions };
11}
12
13async function createSession(userId) {
14  const [user, permissions] = await Promise.all([
15    fetchUser(userId),
16    fetchPermissions(userId)
17  ]);
18
19  return buildSession(user, permissions);
20}
21
22createSession(42).then(console.log).catch(console.error);

This is deterministic: the final block runs only after both upstream blocks are complete.

Handling Partial Failures Safely

Some workflows should continue even when one async task fails. In that case, use Promise.allSettled and inspect statuses.

javascript
1async function loadDashboard(userId) {
2  const results = await Promise.allSettled([
3    fetchUser(userId),
4    fetchPermissions(userId)
5  ]);
6
7  const [userResult, permissionsResult] = results;
8
9  if (userResult.status !== 'fulfilled') {
10    throw new Error('User data is required');
11  }
12
13  const permissions = permissionsResult.status === 'fulfilled'
14    ? permissionsResult.value
15    : [];
16
17  return buildSession(userResult.value, permissions);
18}

This strategy avoids aborting non critical flows while still enforcing requirements for critical data.

Controlling Sequence And Parallelism

A common mistake is writing sequential awaits when tasks are independent. That increases latency because the second task starts only after the first one ends.

Less efficient pattern:

javascript
const user = await fetchUser(userId);
const permissions = await fetchPermissions(userId);

Better parallel pattern:

javascript
const userPromise = fetchUser(userId);
const permissionsPromise = fetchPermissions(userId);
const [user, permissions] = await Promise.all([userPromise, permissionsPromise]);

If you have many tasks and need throttling, combine a queue library or worker pool with this pattern. Start only a controlled number of concurrent operations, then aggregate completion with promise composition.

Testing The Flow

Write tests that verify both success and failure paths. For async orchestration, tests should assert execution order and rejection behavior.

javascript
1import assert from 'node:assert/strict';
2
3async function testCreateSession() {
4  const session = await createSession(7);
5  assert.equal(session.userId, 7);
6  assert.ok(session.permissions.includes('read'));
7}
8
9testCreateSession().then(() => {
10  console.log('test passed');
11});

Tests prevent regressions when a refactor changes promise wiring.

Cancellation, Timeouts, And Cleanup

Production systems often need a timeout guard so one slow dependency does not block the entire workflow forever. You can race the combined promise against a timer and surface a controlled error when the deadline is exceeded. This keeps the orchestration predictable under network instability.

When side effects are involved, define cleanup behavior before you launch concurrent work. For example, if one request writes temporary state and the second request fails, you may need a compensating action. Promise composition handles waiting, but consistency rules are still your responsibility.

Also keep concurrency boundaries explicit. A helper that always returns a promise and never mutates shared external state is easier to compose safely. If shared state is required, isolate writes to one stage after Promise.all resolves so partial completion cannot corrupt state.

Common Pitfalls

  • Using boolean flags and polling loops instead of promise composition.
  • Forgetting to return a promise inside helper functions, causing undefined flows.
  • Mixing callback style and async style in the same chain without clear boundaries.
  • Using sequential awaits for independent operations and adding avoidable latency.
  • Ignoring rejection handling, which can trigger unhandled promise rejections.

Summary

  • Use Promise.all to wait for two async operations before a dependent block.
  • Use Promise.allSettled when partial success is acceptable.
  • Start independent tasks in parallel for lower latency.
  • Keep orchestration logic explicit and test both success and failure paths.
  • Avoid manual synchronization patterns that duplicate built in promise behavior.

Course illustration
Course illustration

All Rights Reserved.