JavaScript
async/await
asynchronous programming
JavaScript programming
web development

I want to use JavaScript async/await

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

async and await are the standard way to write asynchronous JavaScript that stays readable as projects grow. They are built on top of Promises, so they do not introduce a new concurrency model; they make Promise-based code easier to reason about. Instead of chaining many .then() calls, you can express sequential steps in top-to-bottom order and keep error handling in one place. This matters in backend services, browser applications, CLI tools, and test suites where asynchronous code is everywhere.

Many bugs blamed on async/await actually come from misunderstandings about what it does not do. It does not make CPU-bound work faster, it does not run loops in parallel by default, and it does not remove the need for cancellation or timeouts. The sections below cover practical usage patterns and show where developers often trip.

Core Sections

Understand the contract of async

An async function always returns a Promise. Returning a plain value wraps it in Promise.resolve, and throwing creates a rejected Promise.

javascript
1async function getConfig() {
2  return { retries: 3 };
3}
4
5async function failFast() {
6  throw new Error("invalid input");
7}
8
9getConfig().then(console.log); // { retries: 3 }
10failFast().catch(err => console.error(err.message));

This is important when integrating with older code paths that expect callbacks or synchronous return values.

Use await for dependent steps

If step B needs output from step A, await keeps intent clear.

javascript
1async function loadUserDashboard(userId) {
2  const user = await fetch(`/api/users/${userId}`).then(r => r.json());
3  const widgets = await fetch(`/api/widgets?role=${user.role}`).then(r => r.json());
4  return { user, widgets };
5}

This runs sequentially by design. That is correct when dependencies exist. For independent operations, run them concurrently using Promise.all.

javascript
1async function loadPage() {
2  const [profile, notifications] = await Promise.all([
3    fetch("/api/profile").then(r => r.json()),
4    fetch("/api/notifications").then(r => r.json())
5  ]);
6
7  return { profile, notifications };
8}

Centralize error handling

Use try/catch around awaited operations when you need local recovery or custom logging.

javascript
1async function saveOrder(order) {
2  try {
3    const res = await fetch("/api/orders", {
4      method: "POST",
5      headers: { "Content-Type": "application/json" },
6      body: JSON.stringify(order)
7    });
8
9    if (!res.ok) {
10      throw new Error(`Save failed with status ${res.status}`);
11    }
12
13    return await res.json();
14  } catch (err) {
15    console.error("saveOrder failed", { orderId: order.id, err });
16    throw err;
17  }
18}

Let errors bubble when the caller should decide what to do next. Swallowing errors makes failures look like success.

Timeouts and cancellation

await alone will wait indefinitely if a Promise never resolves. For network calls, layer in timeout and cancellation.

javascript
1async function fetchWithTimeout(url, ms = 5000) {
2  const controller = new AbortController();
3  const timeout = setTimeout(() => controller.abort(), ms);
4
5  try {
6    const res = await fetch(url, { signal: controller.signal });
7    return await res.json();
8  } finally {
9    clearTimeout(timeout);
10  }
11}

This avoids hung requests that consume resources and degrade user experience.

Async iteration patterns

A common performance bug is for + await when work can run in parallel.

javascript
1async function fetchAll(ids) {
2  // parallel
3  return await Promise.all(ids.map(id => fetch(`/api/items/${id}`).then(r => r.json())));
4}
5
6async function fetchInOrder(ids) {
7  const results = [];
8  for (const id of ids) {
9    // intentionally sequential
10    results.push(await fetch(`/api/items/${id}`).then(r => r.json()));
11  }
12  return results;
13}

Choose sequential or parallel explicitly; do not rely on incidental behavior.

Common Pitfalls

  • Using await inside Array.prototype.forEach, which does not wait for asynchronous callbacks to finish.
  • Forgetting to return or await a Promise in tests, causing false positives where failing async code is never observed.
  • Running dependent and independent tasks the same way, either over-serializing work or creating race conditions.
  • Catching errors only to log and continue, which hides broken states and makes debugging production incidents harder.
  • Assuming async/await improves CPU-heavy work; for that, use workers, child processes, or different architecture.

Summary

Use async/await as a clarity tool, not magic concurrency. Keep dependent operations sequential with await, run independent operations with Promise.all, and handle errors deliberately with try/catch where recovery makes sense. Add timeouts for I/O so promises cannot hang forever, and be explicit about iteration behavior. Once these patterns are standard in your codebase, asynchronous flows become easier to test, easier to review, and significantly less error-prone.


Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

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

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.