Async functions vs await
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
JavaScript relies heavily on asynchronous operations for tasks like fetching data from servers, reading files, and running timers. Before async/await, developers managed these operations with callbacks and .then() chains, which often became deeply nested and hard to follow. The async and await keywords, introduced in ES2017, let you write asynchronous code that reads almost like synchronous code, making it far easier to reason about.
Async Functions Return Promises
Every function declared with the async keyword automatically wraps its return value in a Promise. This means callers can use .then() on the result or await it from another async function.
If an async function throws an error, the returned Promise rejects instead of resolving. This is the foundation that makes try/catch work inside async functions.
How Await Pauses Execution
The await keyword can only be used inside an async function (or at the top level of an ES module). When the runtime encounters await, it pauses the function's execution until the awaited Promise settles, then resumes with the resolved value.
Think of each await as a checkpoint. The function yields control back to the event loop at that point, allowing other code to run while the Promise is pending. Once the Promise resolves, execution picks up on the next line.
Error Handling With try/catch
Because await unwraps Promises, rejected Promises throw exceptions that you catch with standard try/catch blocks. This replaces the .catch() callback pattern.
Without the try/catch, a rejected Promise would propagate as an unhandled rejection, which modern runtimes treat as an error. Always wrap await calls that might fail.
Sequential vs Parallel Await
A common mistake is awaiting independent Promises one after another when they could run at the same time. Sequential awaits add up the wait times, while Promise.all runs them in parallel.
Use sequential awaits when one operation depends on the result of another. Use Promise.all when the operations are independent and you want to minimize total wait time.
Comparison With .then() Chains
The .then() approach and async/await both work with Promises, but they differ in readability. Here is the same logic written both ways.
The async/await version is easier to step through in a debugger and handles branching logic (if/else) more naturally than nested .then() callbacks.
Common Pitfalls
- Forgetting
await: Calling an async function withoutawaitgives you a Promise object instead of the resolved value, which leads to subtle bugs where comparisons and operations silently pass on the Promise rather than the data. - Using
awaitin a loop when parallelism is possible: Placingawaitinside aforloop serializes every iteration. UsePromise.allwithmapwhen iterations are independent of each other. - Missing error handling: An unhandled rejected Promise inside an async function can crash your Node.js process or produce confusing console warnings in browsers. Always add
try/catchor a.catch()on the caller side. - Blocking the event loop with synchronous work after
await:awaitonly yields while the Promise is pending. If you run CPU-heavy synchronous code after the await resumes, you still block the event loop during that stretch. - Returning
awaitunnecessarily in a return statement: Writingreturn await somePromise()inside atry/catchis useful, but outside atry/catchthe extraawaitadds no benefit. A plainreturn somePromise()forwards the Promise directly.
Summary
- An
asyncfunction always returns a Promise, wrapping whatever value you return. awaitpauses execution of the enclosing async function until the Promise resolves or rejects.- Use
try/catchfor error handling instead of chaining.catch()callbacks. - Reach for
Promise.allwhen you need to run independent asynchronous tasks in parallel rather than sequentially. - The
async/awaitsyntax and.then()chains accomplish the same goal, butasync/awaitproduces code that is easier to read, debug, and maintain.

