As you probably already know, JavaScript executes code line by line in the order that it is written. What happens when you want to control when some parts of your code are executed? If you are already a bit familiar with JavaScript, you might say “callbacks”. While this is true and was the practice until a few years ago, using multiple callbacks can lead to an unwanted and messy situation known as “callback hell”.
Promises were introduced in ECMAScript 2015 (ES6) standard as a more efficient way to handle asynchronous JavaScript operations. But even then, chaining .then() calls could make your code harder to follow.
This is where async and await come in. They make asynchronous JavaScript code blocks look like any other code block, enabling you to read them from top to bottom without constantly breaking your mental flow. The underlying behaviour is still identical to promises, but it makes your code more readable and easier to maintain.
In this blog, we will cover JavaScript async and await in detail, including when to use them, common mistakes to avoid, and even advanced techniques to make your implementation faster and more efficient. Let’s dive in.
Table of Contents
What is async/await in JavaScript?
The async and await keywords were introduced in ECMAScript 2017 (ES2017). They are special keywords in JavaScript that enable you to write asynchronous code in a synchronous syntax. They do not change how JavaScript handles concurrency under the hood, but they give you a cleaner, more readable way to work with Promises.
When you mark a function with the async keyword, JavaScript automatically wraps its return value in a Promise. This means that regardless of what the function returns, it becomes a resolved Promise under the hood. If you throw an error inside an async function in JavaScript, it turns into a rejected Promise.
The await keyword can only be used inside an async function. What it does is pause the execution of that function until the Promise you are waiting for is either resolved or rejected.
This pause only affects the code inside the async function in JavaScript and allows the rest of the application’s code to run without interruption, which can come in handy in a plethora of real-world scenarios like waiting for an API response, file reading, or even a timer.
Syntax of async/await in JavaScript
The async keyword before a function declaration indicates that the function will run asynchronously and always return a Promise. Inside the function, the await keyword is used to pause execution until the asynchronous operation (someAsyncOperation) resolves, and its result can be assigned to a variable.
How async and await in JavaScript Work
Even though async and await keywords in JavaScript make it look like your code is running sequentially, in the background, JavaScript is still handling it asynchronously. The key players to keep in mind here are the event loop and Promises. Let’s understand their parts in detail.
Every time you call an async function in JavaScript, it immediately returns a Promise. Inside the async function, whenever you use the await keyword, JavaScript pauses the execution of that function until the Promise is settled. But don’t be alarmed, it does not affect the rest of your code, and the event loop shall continue to run other tasks in the meantime.
Once the Promise resolves, the result is then passed back into the function, and its execution picks up right where it left off. If the Promise rejects, the error is thrown at that spot, which is why try…catch, which we will discuss later, works so well with async/await.
Here’s a simple breakdown of the process:
- You call an async function – it returns a Promise.
- The code runs until it hits an await.
- The await expression yields control back to the event loop until the Promise is settled.
- When the Promise resolves, the function continues from that point with the resolved value.
- If the Promise rejects, the function throws an error you can catch.
Example
Let’s look at a quick example to gain a better understanding.
Output:
- “Step 1” prints immediately.
- The await tells JavaScript to pause example() but keep running the rest of the program.
- “This runs while waiting…” logs next, showing the event loop is still active.
- After one second, the Promise resolves, and “Step 2: Done” logs.
Replacing JavaScript Promises with Async and Await for Cleaner Code
Here, the first example uses .then() and .catch() to handle a Promise, requiring chained callbacks to process the result. The second example achieves the same with async and await, making the code look synchronous. await pauses execution until the Promise resolves, and try…catch handles errors.
We’ll start with simulating fetching data using setTimeout wrapped in a Promise.
Output:
Now, let’s rewrite the exact same logic using async and await in JavaScript.
Output:
- getData() returns a Promise that resolves after one second.
- In showData(), the await keyword pauses execution until getData() finishes, then assigns the result to result.
- The rest of the program can still run while waiting, only the showData() function is paused.
Convenient, isn’t it? Once you learn to write asynchronous code using async and await keywords, it’s hard to go back to nested callbacks or .then() chains.
JavaScript async/await with Multiple Tasks
One of the great things about async/await is how easy it is to implement multiple asynchronous operations. The trick is to know when to run them sequentially and when to run them in parallel.
1. Sequential Execution
Sometimes, you would want tasks to run in order because each one depends on the result of the other. In which case, you can simply add multiple await calls one after the other. Straightforward isn’t it? Let’s look at an example.
Output:
Here, the second task only starts after the first one finishes, taking about 2 seconds in total.
2. Parallel Execution with Promise.all()
If tasks don’t depend on each other, you can run them at the same time to save time:
Both tasks run together, so the total time is just about 1 second.
Other Promise Utilities
- Promise.allSettled(): waits for all Promises to finish, no matter if they resolve or reject.
- Promise.race(): returns the first Promise to settle (resolve or reject).
- Promise.any(): returns the first Promise that resolves successfully (ignores rejections until all fail).
Example with Promise.allSettled():
This can be handy when you want every operation to run and simply record which ones failed.
A Common Pitfall: await in Loops
Using await directly inside a loop will make the loop run sequentially, which can be slow if you don’t need that. Instead, collect Promises and await them together:
Knowing when to go sequential and when to go parallel can make a huge difference in performance, especially when working with network calls or file operations.
Advanced JavaScript async/await Patterns
Once you have gained a deep understanding of how the async and await keywords in JavaScript work, there are several more advanced patterns that you can use to solve complex problems. Let’s have a look at some of them in detail. And don’t panic if you are not able to understand these at first, as they are very complex and advanced patterns aimed at experienced developers.
1. Async Iterators with for await…of
Sometimes, when you are working with APIs, especially, data comes in pieces. Async iterators let you handle these piece by piece as they become available.
Output:
Here, the loop waits for each new value without blocking the rest of the program.
2. Cancellation with AbortController
When you need to end asynchronous tasks before completion, you can use AbortController to do so.
Note: AbortController works only with APIs that support an AbortSignal (like fetch).
3. Throttling and Batching
If you have hundreds of tasks to run, say, fetching data for a list of users, running them all at once can overwhelm your system or hit rate limits. A batching approach can help:
Here, two tasks run at a time until all are done. This keeps resource usage under control.
While async and await in JavaScript can make your code easier to read and maintain, if you’re not careful, they can also lead to performance issues. The main thing to remember is that await pauses the function until the Promise settles. If you use it at the wrong time, you might accidentally turn fast, parallel work into slow, sequential work.
1. Avoid Unnecessary Sequential Awaits
This takes about 2 seconds, because the second task doesn’t start until the first finishes. If they don’t depend on each other, run them in parallel instead:
Now both tasks are complete in 1 second.
2. Control Concurrency for Large Workloads
Running hundreds of operations at once can overwhelm your system or trigger API rate limits. In those cases, batching or throttling is better. We covered batching earlier, but it’s worth remembering that this is both a performance and stability concern.
3. Be Mindful of Memory Usage
When you await a large Promise result, for example, a big file in memory, that data stays in memory until it’s no longer referenced. If you’re processing huge datasets, consider streaming or processing chunks instead of loading everything at once.
4. Don’t Over-Await
Every await introduces a microtask scheduling step. While it’s fast, calling it unnecessarily can add overhead. If you already have the value, there’s no need to await it.
Performance with async/await comes down to choosing the right execution strategy. Use sequential waits only when order matters, run tasks in parallel when they don’t, and keep an eye on system resources for big workloads.
Error Handling in JavaScript async/await
Regardless of how clean your code looks with async and await in JavaScript, there are many things that can still go wrong: a network might fail, a file might be missing, or maybe some unexpected data causes an exception. This is why error handling is paramount if you don’t want your code or application to throw unexpected errors or even crash.
You probably already know that error handling is done through chaining .catch() for Promises. In async/await try…catch is the most common and efficient way to handle errors.
Here’s a simple example:
Output:
- If the Promise resolves, the code after await runs normally.
- If the Promise rejects, control jumps to the catch block, just like an exception in synchronous code.
You can also still use .catch() with an async function if you prefer:
This approach is shorter, but it’s better suited for handling specific operations in isolation rather than wrapping entire functions.
JavaScript async/await Common Mistakes & How to Avoid Them
Even experienced developers make mistakes while implementing async/await in JavaScript, mostly due to its asynchronous nature. Most of these issues aren’t bugs in JavaScript itself; they come from misunderstanding how asynchronous code behaves. Let’s have a look at some of the common mistakes and how to avoid them.
1. Forgetting await
If you call an async function without await, you get a Promise instead of its result.
Output:
Fix: Add await when you actually need the resolved value.
Edit the codelab for yourself and see how the result changes.
2. Using await inside forEach
forEach doesn’t work well with await because it doesn’t wait for async callbacks to finish.
This runs all tasks in parallel and doesn’t wait for them before moving on.
Fix: Use a for…of loop for sequential waits, or map to Promises and use Promise.all for parallel execution.
3. Ignoring Errors
If a Promise rejects and you don’t catch it, you can get an “unhandled promise rejection” warning.
Fix: Always wrap your awaited code in try…catch or attach a .catch() to the Promise.
4. Mixing Callbacks and Async/Await
Combining callback-based functions with async/await often leads to confusion. If possible, wrap callbacks in a Promise before using them with await.
Output:
Conclusion
Mastering async/await is more than just learning syntax; it’s about thinking in terms of concurrency, efficiency, and clean code structure. The more you practice, the more natural it becomes to spot where asynchronous patterns can make your applications faster and more responsive.If you want to go deeper into JavaScript concepts, from fundamentals to advanced topics like async patterns, modules, and performance optimization, consider joining our complete Full-stack Development course. It’s designed with practical examples, real-world projects, and hands-on exercises to help you master the MERN stack.
Async and Await in JavaScript – FAQs
1. Is async/await blocking?
No. The await keyword in JavaScript only pauses the execution of the code inside the async function to which it is linked. While the function is paused, JavaScript continues running other code in the event loop.
2. Can I use await outside of async functions?
In regular scripts, no, await must be inside an async function. In modern ES modules, you can use top-level await without wrapping it in a function.
3. Is async/await faster than Promises?
Not inherently. Both use Promises under the hood. The advantage of async/await is cleaner, more readable code, not raw performance.
4. Does async/await work in all browsers?
It’s supported in all modern browsers and Node.js versions, but not in older environments. For older browsers, you may need to transpile your code with tools like Babel.
5. How to cancel an async operation?
JavaScript doesn’t have built-in Promise cancellation, but you can design your own cancellation logic with flags or use APIs that support it. For example, AbortController works for some asynchronous operations like timers or streams.