async / await
async/await is syntactic sugar built on top of promises that lets you write asynchronous code that reads top-to-bottom like ordinary synchronous code. Instead of chaining .then() callbacks, you await a promise and the resulting value is assigned to a variable. The control flow becomes linear, errors flow through normal try/catch, and the underlying promise machinery stays exactly the same. This is the modern default for working with asynchronous operations in both the browser and Node.js.
Async functions always return a promise
Marking a function with the async keyword changes one thing about its return value: it is always wrapped in a promise. If you return a plain value, the function returns a promise that fulfills with that value. If you throw, it returns a promise that rejects with that error.
async function getUser() {
return { id: 1, name: "Ada" };
}
const result = getUser();
console.log(result); // a Promise, not the object
getUser().then((user) => console.log(user.name));
Output:
Promise { { id: 1, name: 'Ada' } }
Ada
Because the return value is a promise, an async function is fully interoperable with any promise-based API — you can .then() it, await it, or pass it to Promise.all().
await pauses inside the function
The await operator can only be used inside an async function (or at the top level of an ES module). It pauses execution of that function until the awaited promise settles, then resumes with the fulfilled value. Crucially, it does not block the main thread — the engine is free to run other code while the function is suspended.
async function loadProfile() {
console.log("fetching...");
const res = await fetch("https://api.example.com/user/1");
const user = await res.json();
console.log(`Loaded ${user.name}`);
return user;
}
loadProfile();
console.log("this logs before the fetch resolves");
Output:
fetching...
this logs before the fetch resolves
Loaded Ada
If the awaited promise rejects, await throws the rejection reason as an exception at that line, which you can catch with try/catch.
async function safeLoad() {
try {
const res = await fetch("https://api.example.com/missing");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error("Failed:", err.message);
return null;
}
}
Note:
awaitonly works insideasyncfunctions. At the top level of a<script type="module">or an ES module file you can use top-levelawait, but in a CommonJS file or a regular function you cannot.
Sequential vs parallel awaits
A common performance trap is awaiting independent operations one after another. Each await waits for the previous one to finish before starting the next, even when they don’t depend on each other.
// Sequential: total time = sum of all durations (~3s)
async function sequential() {
const a = await delay(1000, "A");
const b = await delay(1000, "B");
const c = await delay(1000, "C");
return [a, b, c];
}
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
When operations are independent, start them all first and await them together with Promise.all(). The requests now overlap, so the total time is the duration of the slowest one rather than the sum.
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
// Parallel: total time = the longest single duration (~1s)
async function parallel() {
const [a, b, c] = await Promise.all([
delay(1000, "A"),
delay(1000, "B"),
delay(1000, "C"),
]);
return [a, b, c];
}
parallel().then((values) => console.log(values));
Output:
[ 'A', 'B', 'C' ]
| Pattern | When to use | Total time (3 × 1s tasks) |
|---|---|---|
await one at a time | Each step depends on the previous result | ~3s |
await Promise.all([...]) | Steps are independent, need all results | ~1s |
await Promise.allSettled([...]) | Independent, some may fail, want all outcomes | ~1s |
Converting chains to async/await
A promise chain translates almost mechanically into async/await. Each .then(value => ...) becomes an await assignment, and a trailing .catch() becomes a try/catch block.
// Promise chain
function loadOrders(userId) {
return fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((user) => fetch(`/api/orders?customer=${user.id}`))
.then((res) => res.json())
.then((orders) => orders.filter((o) => o.status === "open"))
.catch((err) => {
console.error(err);
return [];
});
}
The async/await version expresses the same logic with flat, readable steps and named intermediate variables.
async function loadOrders(userId) {
try {
const userRes = await fetch(`/api/users/${userId}`);
const user = await userRes.json();
const orderRes = await fetch(`/api/orders?customer=${user.id}`);
const orders = await orderRes.json();
return orders.filter((o) => o.status === "open");
} catch (err) {
console.error(err);
return [];
}
}
Both functions return a promise and behave identically to callers — the rewrite is purely about readability and easier debugging (real stack traces, normal breakpoints, standard try/catch).
Best Practices
- Use
Promise.all()for independent operations; reserve sequentialawaitfor steps that genuinely depend on prior results. - Wrap awaits that can reject in
try/catch, or handle the rejection at the call site — unhandled rejections crash Node processes. - Don’t put
awaitinside afor...ofloop over independent items; map to promises andawait Promise.all()instead. - Remember
awaitonly unwraps the value — checkres.okyourself, sincefetch()does not reject on HTTP error statuses. - Prefer
async/awaitover long.then()chains for new code, but keep them interchangeable since both produce and consume promises. - Use
Promise.allSettled()when you want every result even if some operations fail.