Async/Await
Async/Await
Async/await is syntax built on top of Promises that lets you write asynchronous code that reads like synchronous code. It was standardized in ES2017 and has become the dominant way to handle async operations.
async Functions
The async keyword before a function declaration makes it return a Promise automatically:
async function getUser() {
return { name: "Alice" }; // wrapped in Promise.resolve() automatically
}
getUser().then(console.log); // { name: "Alice" }await
Inside an async function, await pauses execution until the Promise resolves and unwraps its value:
async function main() {
const user = await fetchUser(1); // waits for the Promise
console.log(user.name);
}await can only be used inside an async function (or at the top level of an ES module).
Error Handling with try/catch
Use standard try/catch blocks — they catch Promise rejections transparently:
async function safe() {
try {
const data = await riskyOperation();
return data;
} catch (err) {
console.error(err.message);
} finally {
cleanup();
}
}Running in Parallel
A common mistake is awaiting sequentially when the operations are independent:
// SLOW — sequential (waits for each before starting next)
const a = await fetchA();
const b = await fetchB();
// FAST — parallel (both start at the same time)
const [a, b] = await Promise.all([fetchA(), fetchB()]);The Fetch API
The Fetch API is the modern browser standard for HTTP requests, returning Promises:
async function getPost(id) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json(); // also a Promise
}Code Examples
// Simulated async data fetcher
function getUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id === 1) resolve({ id: 1, name: "Alice", deptId: 10 });
else reject(new Error(`User ${id} not found`));
}, 50);
});
}
function getDepartment(deptId) {
return new Promise((resolve) => {
setTimeout(() => resolve({ id: deptId, name: "Engineering" }), 50);
});
}
async function loadUserProfile(userId) {
try {
const user = await getUser(userId);
const dept = await getDepartment(user.deptId);
console.log(`${user.name} works in ${dept.name}`);
} catch (err) {
console.error("Failed:", err.message);
} finally {
console.log("Load attempt finished");
}
}
loadUserProfile(1);
loadUserProfile(99);async/await makes sequential async operations read like synchronous code. The try/catch block catches any rejection in the await chain, just like a synchronous exception.
const delay = (ms, val) => new Promise((res) => setTimeout(() => res(val), ms));
async function sequential() {
console.time("sequential");
const a = await delay(100, "A");
const b = await delay(100, "B");
const c = await delay(100, "C");
console.timeEnd("sequential");
return [a, b, c];
}
async function parallel() {
console.time("parallel");
const [a, b, c] = await Promise.all([
delay(100, "A"),
delay(100, "B"),
delay(100, "C"),
]);
console.timeEnd("parallel");
return [a, b, c];
}
async function main() {
const s = await sequential();
console.log("Sequential:", s);
const p = await parallel();
console.log("Parallel: ", p);
}
main();Sequential awaits take the sum of all delays. Parallel with Promise.all takes only the maximum delay. When independent async tasks don't depend on each other's results, always run them in parallel.
// Helper: wrap a promise to return [error, data]
async function to(promise) {
try {
const data = await promise;
return [null, data];
} catch (err) {
return [err, null];
}
}
const goodFetch = () => Promise.resolve({ status: 200, data: "payload" });
const badFetch = () => Promise.reject(new Error("Network timeout"));
async function main() {
// Pattern 1: traditional try/catch
try {
const result = await goodFetch();
console.log("Good fetch:", result.data);
} catch (e) {
console.error(e.message);
}
// Pattern 2: Go-style [error, data] tuple
const [err1, data1] = await to(goodFetch());
const [err2, data2] = await to(badFetch());
if (!err1) console.log("Pattern 2 success:", data1.data);
if (err2) console.log("Pattern 2 error:", err2.message);
}
main();The Go-style [error, data] wrapper eliminates nested try/catch blocks when handling multiple async calls. Both patterns are valid — choose based on readability for your use case.
Quick Quiz
1. What does an async function always return?
2. What is wrong with: const a = await fetchA(); const b = await fetchB(); when fetchA and fetchB are independent?
3. Where can the await keyword be used?
Was this lesson helpful?