JavaScript: Async/Await
Write asynchronous code that reads like synchronous code using async functions and the await keyword.
async Functions
Placing the async keyword before a function declaration makes it an async function. It always returns a Promise, even if you return a plain value.
async Function
An async function is a function that implicitly wraps its return value in a resolved Promise.
- Return a plain value: it becomes
Promise.resolve(value) - Throw an error: it becomes
Promise.reject(error) - Works with function declarations, expressions, arrow functions, and class methods
- You can only use
awaitinside anasyncfunction
async Function Basics
The return value is always a Promise chain .then() on it just like any other Promise.
Press Run to execute the code and see output here.
await Keyword
await pauses the async function until the Promise it receives settles, then unwraps the resolved value. The rest of the program continues running while it waits.
await
await unwraps a Promise instead of .then(value => ...), you just write const value = await promise.
- Can only be used inside an
asyncfunction (or at top-level in ES modules) - If the Promise rejects,
awaitthrows the rejection reason as an error - You can await any value non-Promises are returned immediately
- Makes async code read top-to-bottom, just like synchronous code
await in Action
run() pauses at each await, but the code after run() executes right away.
Press Run to execute the code and see output here.
Error Handling with try...catch
When an awaited Promise rejects, it throws an error inside the async function. Wrap the await in atry...catch to handle it cleanly.
try...catch with await
A rejected Promise inside an async function behaves exactly like a thrown error: catch it the same way.
- One
try...catchblock can cover multipleawaitexpressions - Use
finallyfor cleanup that must run whether the await succeeded or failed - Uncaught rejections in async functions become unhandled Promise rejections
- You can also attach
.catch()on the call site:myAsyncFn().catch(err => ...)
try...catch with async/await
A single try block handles errors from any await inside it.
Press Run to execute the code and see output here.
Async/Await vs Promises
Async/await is built on top of Promises: it is not a replacement but a cleaner syntax for the same underlying mechanism. The choice is mostly about readability.
Async/Await vs .then() Chains
Both do the same thing: async/await just reads like synchronous code and is easier to debug.
- async/await produces the same Promise chain under the hood
- Stack traces are cleaner with async/await: the function name appears in errors
- Conditional logic and loops are far easier to write with async/await than with chains
- You still need
Promise.all()for parallel operations: await does not parallelize on its own
Same Logic, Two Styles
The async/await version reads top-to-bottom without nested callbacks.
Press Run to execute the code and see output here.
| Aspect | Promise .then() | async/await |
|---|---|---|
| Readability | Chained: reads left-to-right | Linear: reads top-to-bottom |
| Error handling | .catch() at end of chain | try...catch (familiar pattern) |
| Conditionals/loops | Awkward inside .then() | Natural: just write if/for |
| Debugging | Harder stack traces | Cleaner stack traces |
| Parallel ops | Promise.all([...]) | await Promise.all([...]) |
Parallel Execution with Promise.all()
Using await back-to-back on independent operations runs them one after the other. Wrap them in Promise.all() to start them simultaneously.
Parallel vs Sequential await
Sequential awaits waste time: if two operations do not depend on each other, start them at the same time.
- Sequential:
await a(); await b();takes time(a) + time(b) - Parallel:
await Promise.all([a(), b()])takes max(time(a), time(b)) - Pass an array of Promises (not awaited) to
Promise.all() - Destructure the result array to name each value individually
Parallel Fetch with Promise.all
Both requests fire simultaneously: total wait time is the slower of the two, not their sum.
Press Run to execute the code and see output here.
Sequential vs Parallel Async Operations
Sometimes operations must be sequential (each step depends on the previous result). Other times they are independent and should run in parallel. Knowing which pattern to apply avoids unnecessary slowdowns.
When to Use Each Pattern
Sequential when each step needs the previous result; parallel when steps are independent.
- Sequential: fetch user, then use user.id to fetch their orders
- Parallel: fetch user profile, unread notifications, and recent posts at the same time
- Parallel with
Promise.all()fails fast: one rejection cancels the rest - Use
Promise.allSettled()if you want all results even if some fail
Sequential vs Parallel
Sequential chains results; parallel fires everything at once and waits for all.
Press Run to execute the code and see output here.
Top-level await
In ES modules (.mjs files or files withtype: "module"), you can useawait at the top level without wrapping it in an async function.
Top-level await
Top-level await lets ES modules pause their own execution until an async operation completes before exporting.
- Only works inside ES modules: not in regular scripts or CommonJS files
- The module waits for the await before any importing modules can use its exports
- Useful for: loading config, connecting to a database, or fetching initial data at module load time
- Next.js, Vite, and modern bundlers support top-level await by default
Top-level await
The module pauses until the fetch resolves before continuing or exporting.
Press Run to execute the code and see output here.
Async Iteration: for await...of
for await...of iterates over an async iterable, awaiting each value in turn. It is the async counterpart to for...of.
for await...of
Use for await...of when you have a sequence of Promises or an async generator to process one at a time.
- Works with any object that has
[Symbol.asyncIterator] - Common sources: async generators, Node.js readable streams, paginated API responses
- Each iteration awaits the next value before moving to the next loop body
- Must be used inside an
asyncfunction (or at top-level in a module)
for await...of with an Async Generator
Each yielded value is awaited in sequence: the loop body runs after each resolved yield.
Press Run to execute the code and see output here.
Knowledge Check
1. What does an async function always return?
2. What does await do inside an async function?
3. How do you catch errors when using async/await?
4. What is the main advantage of async/await over .then() chains?
5. Which pattern runs two independent async operations in parallel with async/await?
6. What is top-level await?
7. What does for await...of iterate over?