async programming
concurrent operations
JavaScript promises
non-blocking code
performance optimization

Start async operations, then await later

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

Starting asynchronous work and awaiting it later is a standard way to reduce wasted time in I/O-heavy code. The idea is simple: kick off operations as early as possible, do other useful work while they are in flight, and only await when you actually need the result. This pattern is especially valuable in JavaScript, where many independent network calls are accidentally serialized by overly eager await statements.

Why Early await Can Slow Code Down

await does not block the entire process, but it does pause the current async function until that specific promise settles. If you write a sequence of independent operations with await after each one, you force them to run one after another.

javascript
1async function loadDashboard(userId) {
2  const user = await fetchJson(`/api/users/${userId}`)
3  const orders = await fetchJson(`/api/orders?user=${userId}`)
4  const recommendations = await fetchJson(`/api/recommendations?user=${userId}`)
5
6  return { user, orders, recommendations }
7}

This is correct, but it serializes three unrelated requests. Total runtime becomes roughly the sum of all three latencies.

Start the Promises First

If the operations are independent, start them immediately and keep the promises.

javascript
1async function loadDashboard(userId) {
2  const userPromise = fetchJson(`/api/users/${userId}`)
3  const ordersPromise = fetchJson(`/api/orders?user=${userId}`)
4  const recommendationsPromise = fetchJson(`/api/recommendations?user=${userId}`)
5
6  const featureFlags = computeFeatureFlags(userId)
7
8  const user = await userPromise
9  const orders = await ordersPromise
10  const recommendations = await recommendationsPromise
11
12  return {
13    user,
14    orders,
15    recommendations,
16    featureFlags,
17  }
18}

Now the three requests overlap. The function still awaits all of them, but it waits later, after the requests have already started and after local CPU work has been done.

Use Promise.all When All Results Are Required

If you know you need every result, Promise.all is usually the cleanest form.

javascript
1async function loadDashboard(userId) {
2  const userPromise = fetchJson(`/api/users/${userId}`)
3  const ordersPromise = fetchJson(`/api/orders?user=${userId}`)
4  const recommendationsPromise = fetchJson(`/api/recommendations?user=${userId}`)
5
6  const [user, orders, recommendations] = await Promise.all([
7    userPromise,
8    ordersPromise,
9    recommendationsPromise,
10  ])
11
12  return { user, orders, recommendations }
13}

Promise.all fails fast, which is usually desirable when one missing dependency means the whole operation cannot complete. It also makes it obvious that these promises form one logical group.

Use Promise.allSettled for Partial Results

Sometimes one failure should not cancel everything. For example, a dashboard may still render user data even if a recommendation service is down.

javascript
1async function loadDashboard(userId) {
2  const results = await Promise.allSettled([
3    fetchJson(`/api/users/${userId}`),
4    fetchJson(`/api/orders?user=${userId}`),
5    fetchJson(`/api/recommendations?user=${userId}`),
6  ])
7
8  return {
9    user: results[0].status === 'fulfilled' ? results[0].value : null,
10    orders: results[1].status === 'fulfilled' ? results[1].value : [],
11    recommendations: results[2].status === 'fulfilled' ? results[2].value : [],
12  }
13}

This pattern keeps concurrency but changes the failure policy. That is the important distinction. Concurrency is about timing. all versus allSettled is about error semantics.

Do Not Start Dependent Work Too Early

The pattern only helps when tasks are genuinely independent. If request B needs the result of request A, starting B first is wrong.

javascript
1async function loadInvoice(invoiceId) {
2  const invoice = await fetchJson(`/api/invoices/${invoiceId}`)
3  const customer = await fetchJson(`/api/customers/${invoice.customerId}`)
4  return { invoice, customer }
5}

This sequence is correct because the second URL depends on data from the first response. Trying to parallelize dependent steps usually produces invalid requests, placeholder values, or extra retry logic that is harder to maintain than the original code.

Watch for Unhandled Rejections

If you start a promise and wait a long time before observing it, be deliberate about how errors are handled. In Node.js, a rejected promise can become an unhandled rejection if nothing eventually consumes it in the expected way.

A practical guideline is:

  1. store the promise immediately in a clearly named variable
  2. await it in the same function whenever possible
  3. group related promises with Promise.all or Promise.allSettled

That keeps lifetime and ownership obvious.

This Pattern Exists Outside JavaScript Too

The concept is broader than JavaScript. In C#, you can start Tasks and await them later with Task.WhenAll. In Python, you can create tasks with asyncio.create_task() and await them later. The principle is the same everywhere: launch independent I/O early, then synchronize only when the results are needed.

The key is not “make everything concurrent.” The key is “avoid accidental serialization.”

Common Pitfalls

  • Writing independent async calls with immediate await and unintentionally forcing serial execution.
  • Starting work early even though the later operation depends on data from the earlier one.
  • Ignoring the failure behavior difference between Promise.all and Promise.allSettled.
  • Kicking off promises and letting their ownership become unclear, which makes error handling fragile.
  • Using concurrency for trivial local CPU work where there is no waiting time to hide.

Summary

  • Start independent async operations as soon as possible and await them when their results are actually needed.
  • Storing the returned promises lets I/O overlap instead of running sequentially.
  • 'Promise.all is the usual choice when every result is required.'
  • 'Promise.allSettled is better when partial results are acceptable.'
  • The pattern improves performance only when the operations are independent and I/O-bound.

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.