11 min read· by Awab Tech Lover

Mastering Async/Await: Common JavaScript Mistakes

Avoid common async await mistakes in JavaScript with this practical troubleshooting guide. Learn to write robust asynchronous code efficiently.

Mastering Async/Await: Common JavaScript Mistakes

Mastering Async/Await: Common JavaScript Mistakes and How to Fix Them

JavaScript's async/await syntax has revolutionized asynchronous programming, making complex operations feel more synchronous and readable. It's built on Promises, offering a much cleaner way to handle tasks like fetching data, reading files, or interacting with databases without getting lost in callback hell or deeply nested .then() chains. However, despite its elegance, there are several common async await mistakes in JavaScript that developers frequently encounter. Understanding these pitfalls and knowing how to troubleshoot them is crucial for writing robust, efficient, and bug-free code.

This guide will walk you through the most prevalent async/await errors, explain why they happen, and provide practical, example-driven solutions to help you write better asynchronous JavaScript.

1. Forgetting to Use await

One of the most fundamental async await mistakes in JavaScript is simply forgetting to use the await keyword. When you call an async function, it always returns a Promise. If you don't await that Promise, you'll end up working with the Promise object itself, rather than its resolved value.

The Mistake:

async function fetchData() {
  return Promise.resolve("Data fetched!");
}

async function processData() {
  const data = fetchData(); // Mistake: Missing 'await'
  console.log(data); // Output: Promise { <pending> }
}

processData();

In this scenario, data receives the pending Promise returned by fetchData(), not the "Data fetched!" string.

The Fix:

Always use await when you want to pause execution until a Promise resolves and get its value.

async function fetchData() {
  return Promise.resolve("Data fetched!");
}

async function processData() {
  const data = await fetchData(); // Correct: 'await' pauses execution
  console.log(data); // Output: Data fetched!
}

processData();

2. Using await Outside an async Function

The await keyword is only valid inside an async function. Attempting to use it in a regular function or at the top level of a script (outside of an immediately invoked async function expression, or IIAFE) will result in a SyntaxError.

The Mistake:

function loadUser() {
  const user = await getUserFromAPI(); // SyntaxError: await is only valid in async functions
  console.log(user);
}

The Fix:

Ensure that any function containing await is explicitly declared as async. For top-level execution, wrap your await calls in an IIAFE.

async function getUserFromAPI() {
  return Promise.resolve({ id: 1, name: "Alice" });
}

async function loadUser() { // Correct: function is async
  const user = await getUserFromAPI();
  console.log(user);
}

loadUser();

// Or for top-level await (requires module context or specific JS engine support):
// Alternatively, use an IIAFE:
(async () => {
  const topLevelData = await getUserFromAPI();
  console.log("Top-level data:", topLevelData);
})();

Note: Top-level await is a relatively new feature and might not be supported in all environments (e.g., older Node.js versions or non-module scripts in browsers).

3. Incorrect Error Handling with try...catch

While async/await makes error handling much cleaner than .catch(), forgetting to wrap await calls in a try...catch block means unhandled Promise rejections will crash your application (or be caught by a global error handler, which is often too late). This is a critical async await mistake in JavaScript for robust applications.

The Mistake:

async function mightFail() {
  return Promise.reject("Operation failed!");
}

async function executeOperation() {
  const result = await mightFail(); // If this rejects, it's an unhandled promise rejection
  console.log(result);
}

executeOperation(); // This will log an unhandled promise rejection error

The Fix:

Always use try...catch blocks around await calls that might reject.

async function mightFail() {
  // Simulate an error after a delay
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("Operation failed after 1 second!");
    }, 1000);
  });
}

async function executeOperation() {
  try {
    const result = await mightFail();
    console.log(result);
  } catch (error) {
    console.error("Caught an error:", error); // Output: Caught an error: Operation failed after 1 second!
  }
}

executeOperation();

This approach allows you to gracefully handle errors without stopping your program's execution.

4. Sequential Execution When Parallel is Possible

One of the most common performance-related async await mistakes in JavaScript is awaiting multiple independent Promises sequentially when they could run in parallel. While await makes code look synchronous, it also makes it behave synchronously in terms of execution flow.

The Mistake:

async function getUser(id) {
  console.log(`Fetching user ${id}...`);
  return new Promise(resolve => setTimeout(() => resolve({ id, name: `User${id}` }), 1000));
}

async function getAllUserDataSequential() {
  console.time("Sequential Fetch");
  const user1 = await getUser(1); // Waits 1 second
  const user2 = await getUser(2); // Waits another 1 second
  const user3 = await getUser(3); // Waits yet another 1 second

  console.log(user1, user2, user3);
  console.timeEnd("Sequential Fetch"); // Approx. 3 seconds
}

getAllUserDataSequential();

This code takes roughly 3 seconds to complete because each await pauses execution until the previous Promise resolves.

The Fix:

Use Promise.all() to run independent asynchronous operations concurrently. Promise.all() takes an array of Promises and returns a single Promise that resolves when all of the input Promises have resolved, or rejects if any of them reject.

async function getUser(id) {
  console.log(`Fetching user ${id}...`);
  return new Promise(resolve => setTimeout(() => resolve({ id, name: `User${id}` }), 1000));
}

async function getAllUserDataParallel() {
  console.time("Parallel Fetch");
  const promises = [getUser(1), getUser(2), getUser(3)]; // Start all promises concurrently
  const [user1, user2, user3] = await Promise.all(promises); // Await all results

  console.log(user1, user2, user3);
  console.timeEnd("Parallel Fetch"); // Approx. 1 second
}

getAllUserDataParallel();

For cases where you don't care if some Promises fail and just want the results of those that succeed, Promise.allSettled() is a great alternative. It returns an array of objects, each describing the outcome of each Promise (fulfilled or rejected).

async function getPossiblyFailingData() {
  const results = await Promise.allSettled([
    Promise.resolve(1),
    Promise.reject('Error!'),
    Promise.resolve(3)
  ]);
  console.log(results);
  /* Output:
  [
    { status: 'fulfilled', value: 1 },
    { status: 'rejected', reason: 'Error!' },
    { status: 'fulfilled', value: 3 }
  ]
  */
}
getPossiblyFailingData();

5. Not Handling Promise Rejections in Promise.all()

When using Promise.all(), it's crucial to remember that if any of the Promises in the array reject, Promise.all() immediately rejects with the reason of the first rejected Promise. This can lead to unhandled rejections if not caught.

The Mistake:

async function fetchItem(id) {
  return new Promise((resolve, reject) => {
    if (id === 2) {
      setTimeout(() => reject(`Failed to fetch item ${id}`), 500); // This will reject
    } else {
      setTimeout(() => resolve(`Item ${id} data`), 1000);
    }
  });
}

async function getItems() {
  const items = await Promise.all([fetchItem(1), fetchItem(2), fetchItem(3)]); // Error will propagate
  console.log("All items:", items);
}

getItems(); // This will result in an unhandled promise rejection

The Fix:

Wrap Promise.all() in a try...catch block.

async function fetchItem(id) {
  return new Promise((resolve, reject) => {
    if (id === 2) {
      setTimeout(() => reject(`Failed to fetch item ${id}`), 500);
    } else {
      setTimeout(() => resolve(`Item ${id} data`), 1000);
    }
  });
}

async function getItemsSafely() {
  try {
    const items = await Promise.all([fetchItem(1), fetchItem(2), fetchItem(3)]);
    console.log("All items:", items);
  } catch (error) {
    console.error("One or more items failed to fetch:", error); // Output: One or more items failed to fetch: Failed to fetch item 2
  }
}

getItemsSafely();

Alternatively, as mentioned before, use Promise.allSettled() if you want to proceed even if some promises fail, and handle individual results.

6. Confusion with forEach and map for Asynchronous Operations

Using forEach directly with async/await can be deceptive, as forEach does not wait for asynchronous callbacks to complete. This is a common async await mistake in JavaScript for loop patterns.

The Mistake:

async function processRecord(record) {
  console.log(`Processing ${record}...`);
  return new Promise(resolve => setTimeout(() => {
    console.log(`Finished ${record}`);
    resolve();
  }, 1000));
}

async function processAllRecordsWrong() {
  const records = ["A", "B", "C"];
  console.time("forEach Async");
  records.forEach(async record => {
    await processRecord(record);
  });
  console.timeEnd("forEach Async"); // Will finish almost immediately, not waiting for processing
  console.log("All records scheduled (but not necessarily finished)!");
}

processAllRecordsWrong();

The console.timeEnd will show a very short duration because forEach iterates and starts the async functions, but doesn't await them. The outer processAllRecordsWrong function simply finishes without waiting.

The Fix:

For awaiting multiple asynchronous operations, use Promise.all() with map(). The map() function will return an array of Promises, which Promise.all() can then await.

async function processRecord(record) {
  console.log(`Processing ${record}...`);
  return new Promise(resolve => setTimeout(() => {
    console.log(`Finished ${record}`);
    resolve();
  }, 1000));
}

async function processAllRecordsCorrect() {
  const records = ["A", "B", "C"];
  console.time("map + Promise.all Async");
  const processingPromises = records.map(async record => {
    return await processRecord(record);
  });
  await Promise.all(processingPromises); // Await all promises generated by map
  console.timeEnd("map + Promise.all Async"); // Will take approx. 1 second (parallel processing)
  console.log("All records finished processing!");
}

processAllRecordsCorrect();

If you need sequential processing (e.g., due to dependencies or rate limits), a for...of loop with await is appropriate:

async function processAllRecordsSequentially() {
  const records = ["X", "Y", "Z"];
  console.time("for...of Async");
  for (const record of records) {
    await processRecord(record); // Awaits each record sequentially
  }
  console.timeEnd("for...of Async"); // Will take approx. 3 seconds
  console.log("All records finished processing sequentially!");
}
processAllRecordsSequentially();

7. Not Returning Promises in async Functions

An async function always returns a Promise. If your async function itself calls another async function, you might think the inner function's Promise is automatically handled. However, if you don't await or return the inner Promise, its completion (or rejection) might not propagate correctly to the caller.

The Mistake:

async function doSomethingAsync() {
  // Simulates an async operation
  return new Promise(resolve => setTimeout(() => resolve("Inner task done"), 500));
}

async function orchestrateTasks() {
  doSomethingAsync(); // Mistake: Not awaited or returned
  console.log("Orchestration finished."); // This will log *before* "Inner task done"
}

orchestrateTasks();
// Output:
// Orchestration finished.
// (after 500ms) Inner task done

The orchestrateTasks function isn't waiting for doSomethingAsync to complete.

The Fix:

await the inner async function, or return its Promise.

async function doSomethingAsync() {
  return new Promise(resolve => setTimeout(() => resolve("Inner task done"), 500));
}

async function orchestrateTasksCorrect() {
  const result = await doSomethingAsync(); // Correct: Await the inner function
  console.log(result); // Output: Inner task done
  console.log("Orchestration finished.");
}

orchestrateTasksCorrect();
// Output:
// (after 500ms) Inner task done
// Orchestration finished.

Or, if you simply want to propagate the Promise:

async function orchestrateTasksReturnPromise() {
  return doSomethingAsync(); // Correct: Return the Promise
}

orchestrateTasksReturnPromise().then(result => {
  console.log(result); // Output: Inner task done
  console.log("Orchestration finished.");
});

Summary of Common Async/Await Mistakes

Mistake Type Description Consequence Recommended Fix
Forgetting await Calling an async function without await. Variable holds a Promise, not its resolved value. Always await an async function call when you need its resolved value.
await outside async Using await in a non-async function or top-level script. SyntaxError. Declare the function as async; use an IIAFE for top-level await if necessary.
No try...catch Not wrapping await calls that might reject. Unhandled Promise rejection, application crash. Use try...catch blocks around await calls for robust error handling.
Sequential await Awaiting independent Promises one after another. Performance bottleneck, increased execution time. Use Promise.all() for concurrent execution of independent Promises.
Promise.all() rejection Not handling rejections when using Promise.all(). Unhandled Promise rejection if any nested Promise fails. Wrap Promise.all() in try...catch; consider Promise.allSettled() if partial success is acceptable.
forEach with async/await Using forEach loop with async callbacks. forEach does not wait for asynchronous operations to complete. Use map() with Promise.all() for concurrent ops, or for...of for sequential ops.
Not returning/awaiting nested Promises An async function calls another async function but doesn't await or return its result. Outer function completes before inner function, logic errors. await the inner Promise or return it to propagate its resolution/rejection.

Conclusion

Async/await is a powerful tool in modern JavaScript, simplifying complex asynchronous patterns. However, like any powerful feature, it comes with its own set of nuances and potential pitfalls. By understanding and actively avoiding these common async await mistakes in JavaScript, you can write more reliable, performant, and maintainable code. Always remember the fundamental principles: async functions return Promises, await pauses execution for a Promise, and robust error handling is paramount. Master these concepts, and you'll be well on your way to becoming an asynchronous JavaScript expert.

FAQ

Q1: What is the main benefit of using async/await over traditional Promises?

A1: The main benefit is improved readability and maintainability. async/await allows you to write asynchronous code that looks and behaves more like synchronous code, making complex Promise chains easier to reason about and debug, especially when dealing with multiple sequential operations or error handling.

Q2: Can I use await in a regular JavaScript function?

A2: No, the await keyword can only be used inside an async function. If you try to use it in a non-async function, you will get a SyntaxError. If you need to use await at the top level of your script, you can wrap your code in an Immediately Invoked Async Function Expression (IIAFE) or use top-level await if your environment supports it (e.g., in ES modules).

Q3: How do I handle errors with async/await?

A3: Errors with async/await are best handled using try...catch blocks, similar to synchronous code. You wrap the await calls that might throw an error (or return a rejected Promise) within a try block, and handle any caught errors in the catch block.

Q4: When should I use Promise.all() with async/await?

A4: You should use Promise.all() when you have multiple independent asynchronous operations that you want to run concurrently and wait for all of them to complete. This greatly improves performance by preventing unnecessary sequential waiting. If any of the promises passed to Promise.all() reject, the entire Promise.all() will reject.

Q5: What's the difference between Promise.all() and Promise.allSettled()?

A5: Promise.all() resolves only if all given Promises resolve, and it rejects immediately if any Promise rejects. Promise.allSettled(), on the other hand, waits for all given Promises to settle (either resolve or reject) and returns an array of objects describing the outcome of each Promise. Use allSettled() when you need to know the result of every Promise, regardless of whether some failed.

Q6: Why does forEach not work well with async/await for parallel operations?

A6: Array.prototype.forEach is designed for synchronous iteration. When you pass an async callback to forEach, it will execute the callback for each item without waiting for the previous async operation to complete. This means your forEach loop will finish almost immediately, even though the asynchronous tasks it initiated are still running in the background. To wait for all asynchronous operations in a collection, you should use Array.prototype.map to create an array of Promises and then await Promise.all() on that array.