async/await
Learn how the async and await keywords let you write asynchronous, promise-based code that reads like synchronous code, including error handling with try/catch and the difference between sequential and parallel async operations.
async/await, introduced in ES2017, is syntactic sugar built directly on top of promises. It doesn't replace promises — every async function still returns a promise under the hood — but it lets you write asynchronous logic using ordinary-looking, top-to-bottom code instead of chains of .then() calls.
The async keyword
Placing async before a function declaration (async function getData() { ... }) does two things: it allows the await keyword to be used inside that function, and it guarantees the function always returns a Promise — even if you write a plain return statement with a non-promise value, JavaScript automatically wraps it in a resolved promise.
If code inside an async function throws an error (or a synchronous exception occurs), the promise that the async function returns is automatically rejected with that error, rather than the error escaping and crashing the program the way an uncaught synchronous throw would.
The await keyword
await can only be used inside an async function (or, in modern JavaScript modules, at the top level). Placing await in front of a promise pauses execution of that async function at that line — without blocking the rest of the program — until the promise settles, and then it 'unwraps' the result: a fulfilled promise's value becomes the expression's value directly, no .then() required.
Under the hood, await is still using the promise machinery and the microtask queue from the previous lessons: pausing at await doesn't freeze the call stack, it simply schedules the rest of the async function's body to resume as a microtask once the awaited promise settles.
Error handling with try/catch
Because await makes an asynchronous operation look and behave like a normal statement, you can wrap it in an ordinary try/catch block, exactly as you would for synchronous errors. If the awaited promise rejects, the throw happens right at the await expression, and control jumps to the matching catch block.
This is a major usability win over promise chains: instead of scattering .catch() handlers across a chain (or forgetting one), you get one familiar try/catch construct that can wrap several sequential await steps at once.
Sequential vs parallel async operations
Writing await one after another (await a(); await b();) runs those operations sequentially — b does not even start until a has fully finished. This is correct when b genuinely depends on a's result, but it's wasteful when the two operations are actually independent, because you're needlessly waiting for the first to finish before even starting the second.
When operations don't depend on each other, start them all at once (without awaiting immediately), collect the resulting promises, and then await them together — commonly using Promise.all (covered in depth in the next lesson). This lets independent asynchronous operations run concurrently instead of one strictly after another, which can significantly cut down total wait time.
async function and await
await fetch(...) pauses until the network request settles and unwraps the resolved Response object directly into the response variable. getUserData itself still returns a Promise — callers must use await or .then() to get the actual user object out of it. try/catch around the await lines catches both a rejected fetch and a rejected response.json() call, in one place.
async function getUserData(id) {
try {
const response = await fetch("/api/users/" + id);
const user = await response.json();
return user;
} catch (error) {
console.log("Failed to load user:", error.message);
throw error;
}
}Code Examples
delay wraps setTimeout in a promise so it can be awaited. Inside the async function run, await delay(...) pauses that function's execution at that line for about 1 second, then resumes with the resolved value assigned to result — reading top to bottom just like synchronous code. Expected result: Start (about 1 second pause) done waiting End await delay(1000, "done waiting") looks like a normal blocking call but does not block the rest of the program — other code outside run() can still execute while it waits.
When riskyOperation(true) rejects, the await expression effectively throws inside the try block, so control jumps straight to the catch block instead of continuing to the console.log(result) line. Expected result: Caught error: The operation failed Attempt finished.
A rejected awaited promise behaves exactly like a thrown exception at that line, which is why ordinary try/catch/finally works around it.
In sequential(), the second loadResource call doesn't even start until the first await finishes, so the two 500ms waits add up to roughly 1000ms. In parallel(), both loadResource calls start immediately (creating two promises right away), and only then does the function await each one — so the two 500ms waits overlap, and the total time is roughly 500ms instead of 1000ms.
Starting both promises before awaiting either one is what makes the two waits overlap instead of stack — the key technique for parallel async operations.
Common mistake: A very common performance mistake is writing const a = await loadResource("A", 500); const b = await loadResource("B", 500); for two genuinely independent operations, unnecessarily doubling the total wait time.
function delay(ms, value) {
return new Promise(resolve => setTimeout(() => resolve(value), ms));
}
async function run() {
console.log("Start");
const result = await delay(1000, "done waiting");
console.log(result);
console.log("End");
}
run();function riskyOperation(shouldFail) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject(new Error("The operation failed"));
} else {
resolve("Success!");
}
}, 300);
});
}
async function attempt() {
try {
const result = await riskyOperation(true);
console.log(result);
} catch (error) {
console.log("Caught error:", error.message);
} finally {
console.log("Attempt finished.");
}
}
attempt();function loadResource(name, ms) {
return new Promise(resolve => setTimeout(() => resolve(name), ms));
}
async function sequential() {
const a = await loadResource("A", 500);
const b = await loadResource("B", 500);
console.log(a, b); // takes ~1000ms total
}
async function parallel() {
const aPromise = loadResource("A", 500);
const bPromise = loadResource("B", 500);
const a = await aPromise;
const b = await bPromise;
console.log(a, b); // takes ~500ms total
}Sequential vs parallel await
Awaiting one promise at a time forces independent operations to run back-to-back. Starting both operations first and awaiting them afterward lets their waiting periods overlap.
Sequential:
await A (500ms) ────► await B (500ms) ────► done
|────────────── ~1000ms total ──────────────|
Parallel (start both first, then await):
A started ──────────► (500ms) ─┐
B started ──────────► (500ms) ─┴─► both done
|──────── ~500ms total ────────|Important Rules
async before a function makes it always return a Promise, and allows await to be used inside it.
await pauses an async function at that line until the awaited promise settles, without blocking the rest of the program.
A rejected awaited promise behaves like a thrown exception, catchable with an ordinary try/catch block.
Multiple sequential await calls run one after another; start independent promises before awaiting them to run the waits in parallel.
await can only be used inside an async function (or at the top level of a module).
Where this shows up
async/await is the standard way modern JavaScript code consumes fetch() responses, database queries in Node.js backends, and file system operations — most production codebases prefer it over raw .then() chains because it reads top-to-bottom and integrates cleanly with try/catch, making asynchronous logic much easier to follow and debug.
function getData() { const result = await fetch("/api/data"); return result; } async function getData() { const result = await fetch("/api/data"); return result; } await is only legal inside a function declared async (or at a module's top level) — using it in a plain function is a syntax error.
async function loadBoth() { const a = await loadResource("A", 500); const b = await loadResource("B", 500); } async function loadBoth() { const aPromise = loadResource("A", 500); const bPromise = loadResource("B", 500); const a = await aPromise; const b = await bPromise; }
When A and B don't depend on each other, awaiting them one at a time forces their wait times to add up instead of overlapping — starting both first lets them run concurrently.
[Multiple Choice] Why does starting both promises before awaiting either one make two independent async operations faster than awaiting them one at a time? A. It runs the operations on separate threads B. It skips error handling, which saves time C. Both operations begin immediately and their waiting periods overlap, instead of the second waiting for the first to fully finish first D. await automatically parallelizes any operations written after it
C. Both operations begin immediately and their waiting periods overlap, instead of the second waiting for the first to fully finish first — Calling both async functions before awaiting either one starts both underlying operations right away, so their wait times overlap — awaiting one at a time forces the second to wait for the first to completely finish before even starting.
Write an async function loadProfile() that awaits two independent helper functions, loadUser() and loadSettings() (each returning a promise that resolves after a delay), started in parallel rather than sequentially, and logs both results together once both are done. Wrap it in a try/catch that logs any error.
async function loadProfile() { try { const userPromise = loadUser(); const settingsPromise = loadSettings(); const user = await userPromise; const settings = await settingsPromise; console.log(user, settings); } catch (error) { console.log("Failed to load profile:", error.message); } }
Call both helper functions first without awaiting immediately, store the two promises in variables, and only then await each one.