async & await
Write promise code that reads from top to bottom: async functions, await pauses, error handling, top-level await, and the promise machinery underneath.
- 01Read async functions preciselyExplain why every async function returns a new promise and how returns, throws, promises, and thenables are adopted.
- 02Predict await orderStep through the bookmark pause: the caller continues, then the async function resumes later with local variables intact.
- 03Handle failures honestlyUse
try,catch,finally, andreturn awaitwhere they actually change behavior.
Readable promise code
Promises gave JavaScript a standard way to represent work that will finish later. async and await give you a friendlier way to write that promise code. Instead of nesting every next step inside .then(...), you can write a sequence that reads from top to bottom.
An async function always returns a promise. Inside it, await pauses that function until a value is ready, then resumes the function later with the fulfilled value—or throws the rejection at that line.
The pause is the magic worth understanding. It does not freeze the browser tab, block clicks, or stop timers. It pauses one async function, lets the rest of JavaScript keep moving, and comes back to the exact line when the awaited promise settles. If you have already studied Promises, the Event loop, and Microtasks, this lesson connects those pieces into the syntax you will use every day.
Imagine reading a recipe, putting a bookmark on step four, and leaving the kitchen while the dough rises. When you return, the ingredients, bowl, and notes are still there. await behaves like that bookmark for one async function.
- In real life: Place a bookmark in a book
- In JavaScript: Reach an
awaitline - In real life: Go answer the door or make tea
- In JavaScript: The engine runs other ready JavaScript
- In real life: Return to the same page with the plot still in mind
- In JavaScript: Resume the async function with its local variables intact
Where the analogy stops: A bookmark waits because a human left the book. JavaScript resumes only when the awaited value settles and the continuation is scheduled; it is not a manual pause button you control.
We will cover the pieces in the order you need them: async functions return promises, await pauses and resumes, errors travel through try/catch, top-level await belongs to modules, and under the hood the whole feature is promise continuation machinery—not a second thread.
Async functions wrap results in promises
INTERACTIVEPut async before a function declaration, function expression, arrow function, or method, and calling it always returns a new native promise. The body begins running synchronously, right away, until it either returns, throws, or reaches its first await.
Whatever leaves an async function is placed in a promise box. A value becomes a fulfilled box. A thrown error becomes a rejected box. If you hand it another promise-like box, the outer box follows that inner box instead of nesting boxes forever.
- In real life: Return a toy
- In JavaScript:
return 42fulfills the promise with42 - In real life: Return a boxed gift from another counter
- In JavaScript:
return somePromisemakes the outer promise adopt it - In real life: Return a box with a claim ticket
- In JavaScript: Returning a thenable calls its
thenand adopts the result - In real life: Drop the gift and mark it broken
- In JavaScript:
throw errorrejects the returned promise
Where the analogy stops: Real gift wrap changes only packaging. Async functions also schedule promise reactions, so when the wrapping settles can matter.
Try the branches below. The code is fixed, but you choose which branch runs in the real browser. Notice two details: return; fulfills with undefined, and returning a promise does not return the same promise object—it creates a new async-function promise that adopts the result.
async function gift(kind) { if (kind === "value") return 42; if (kind === "promise") return Promise.resolve("ready"); if (kind === "thenable") return { then(resolve) { resolve("thenable value"); } }; if (kind === "throw") throw new Error("broken ribbon");} gift(kind);Choose what the async function does, then run it in this browser.
| What the body does | Returned promise state | Value or reason |
|---|---|---|
return 42 | fulfilled | 42 |
return Promise.resolve("ready") | fulfilled after adoption | "ready" |
return { then(resolve) { ... } } | fulfilled after thenable adoption | whatever the thenable resolves |
throw new Error("broken") | rejected | that error |
| no return | fulfilled | undefined |
An unawaited async call still starts. If it later throws and nobody catches the returned promise, the host reports an unhandled rejection. In real code, either await it, return it to someone who will await it, or attach .catch(...) deliberately.
Await pauses one function, not the whole café
STEP THROUGHThe most important output pattern is A, C, B. The async function prints A before the await. The caller continues and prints C. Later, the async function resumes and prints B.
You take your token and sit down while the counter keeps serving other people. When your number appears, you collect your food; await pauses only this function while JavaScript keeps working.
- In real life: Take your McDonald's order token
- In JavaScript: Reach
awaitwith a pending promise - In real life: The counter serves other people
- In JavaScript: JavaScript handles other ready work
- In real life: Your number appears on the screen
- In JavaScript: The promise settles and queues continuation
- In real life: Collect your food
- In JavaScript: The async function resumes with the value
Where the analogy stops: A counter has many people. JavaScript on a page usually has one main thread; await yields instead of creating another thread.
Predict the order. Step through the real sequence: the async function starts now, pauses at await, and resumes later.
script
async function demo() { console.log("A: before await"); const value = await Promise.resolve("B"); console.log(value + ": after await");} console.log("C: caller continues");Try all three settings. A resolved promise, a timer promise, and a plain value all pause the rest of the async function. The timer changes how long you wait, not the shape of the order. Even await 0 means “wrap this with Promise.resolve and resume later.” Do not count exact ticks; modern engines have optimized await over time. The stable rule is: code after await runs later, after the current caller can continue.
When an async function pauses, its local variables are saved as part of the continuation. That is why you can write const user = await getUser(); and then use user on the next line as if the function had simply waited there.
try/catch with await
STEP THROUGHA rejected promise awaited inside a try behaves like a throw at that line. That means ordinary catch and finally become useful again for asynchronous work.
Choose which step rejects. Step through how try, catch, and finally react.
script
try { const user = await step("user", failStep); const posts = await step("posts", failStep); return user + " + " + posts; } catch (error) { return "caught " + error.message; } finally { cleanup(); }}Two traps are classics. First, return fetchData() inside a try returns the promise without awaiting it. If that promise rejects later, the catch has already been skipped. Use return await fetchData() when you need that catch (or the timing of finally) to observe the failure.
async function notCaught() { try { return fetchData(); } catch (error) { return "caught"; }} async function caught() { try { return await fetchData(); } catch (error) { return "caught"; }}Outside a try/catch, return await is usually unnecessary: returning the promise is enough. Inside a try, it can be the difference between “caught here” and “rejected somewhere else.”
Second, forgetting await can make checks silently wrong. A promise object is truthy, so if (checkPermission()) runs the branch before the permission boolean arrives.
if (checkPermission()) { showAdminButton(); // runs: a Promise object is truthy} if (await checkPermission()) { showAdminButton(); // waits for the boolean}What await is roughly doing under the hood
COMPAREYou should not imagine engine internals too literally. Different engines implement optimizations differently. But as a mental model, await value is roughly:
- convert
valuewithPromise.resolve(value), - save the async function’s current state, and
- run the rest of the function as a continuation when the promise settles.
async function label() { const user = await getUser(); return "Hi, " + user.name;} function labelRoughly() { return Promise.resolve(getUser()).then((user) => { return "Hi, " + user.name; });}That saved continuation is why async functions feel like suspended state machines, similar in spirit to generators: the function can stop in the middle, keep its locals, and later continue. The promise chain version is not a perfect source-to-source translation, but it explains why await does not block the thread and why errors after await become promise rejections.
| Question | await | .then chain |
|---|---|---|
| Where does the next step live? | Next line in the async function | Callback passed to .then |
| How are failures caught? | try/catch around awaited lines | .catch or rejection callback |
| Does it block the thread? | No | No |
| What value is returned by the outer function? | A promise | Usually a promise returned from the chain |
Top-level await
SANDBOXSince ES2022, ES modules can use await at the top level, outside any function. This pauses evaluation of that module and of modules that import it. Classic scripts cannot do this. DevTools consoles and the Node REPL often allow top-level await as a learning convenience, but that is not the same as putting it in a classic script file.
<script type="module"> const data = await Promise.resolve("module data"); parent.postMessage({ kind: "module", value: data }, "*");</script> <script> await Promise.resolve("classic data"); // SyntaxError</script>The module iframe will post after its top-level await resolves.
The classic iframe will report whether top-level await parsed.
Two sandboxed iframes run predefined code. The parent accepts messages only from those exact frames. The module awaits successfully; the classic script reports a syntax error in most browsers.
allow-scripts and srcdoc. DevTools consoles may allow top-level await as a convenience; classic script files do not.Top-level await is helpful for module setup, such as loading a configuration before exporting values. Use it carefully: an importer waits for the awaited module, so a slow top-level await can delay a whole module graph.
- Inside an
async functionbody - Top level of an ES module
- Top level of a classic script tag
items.forEach((x) => { await save(x); })items.forEach(async (x) => { await save(x); })const load = async () => await getData();async save() { await api.save(this); }
Sort each location by whether await is allowed and whether it waits the thing people expect.
Where you’ll use this
You will use async/await any time a program asks for something that is not ready yet: data from a server, a file from disk, an animation finishing, a permission prompt, or a timer. The common shape is “show loading, await the work, render success, and handle failure.”
async function showDashboard() { setStatus("Loading..."); try { const user = await getUser(); const projects = await getProjects(user.id); renderDashboard(user, projects); } catch (error) { showError("Could not load the dashboard."); } finally { hideSpinner(); }}That example waits sequentially: projects need the user’s id first. When two requests do not depend on each other, you often start them together and await them together. The next lesson, “Sequential vs parallel,” focuses on those patterns, including loops. Later, the course returns to for await...of for streams of async values.
Common misconceptions
- “async makes the function run on another thread.” No: the function starts on the same JavaScript thread and yields at awaits.
- “await blocks the whole page.” It pauses one async function; other ready code can run.
- “awaiting a non-promise is synchronous.” It still pauses and resumes later.
- “try/catch catches every promise returned inside it.” Only awaited rejections throw at that line. Returning a promise is not the same as awaiting it.
- “await inside forEach makes forEach wait.” An async callback is allowed, but
forEachdoes not wait for its promises. Preview: the next lesson fixes this with explicit loop and parallel patterns. - “Top-level await works in any script.” Standard top-level await is for modules, not classic scripts.
Practice exercises
5 EXERCISESRead the code and type the three words in the order they print.
async function run() {
console.log("one");
await 0;
console.log("three");
}
run();
console.log("two");The output is one, then two, then three. The first log happens before await, the caller continues, and the final log runs in the continuation.
What value does the .then callback log?
async function answer() {
return 12;
}
answer().then((value) => console.log(value));.then receives 12. return 12 in an async function fulfills the returned promise with 12.
Type the word that prints. Then explain why the inner catch did not run.
function fetchData() {
return Promise.reject(new Error("nope"));
}
async function demo() {
try {
return fetchData();
} catch (error) {
return "caught";
}
}
demo().catch((error) => console.log(error.message));It prints nope. The rejection is not caught inside demo because fetchData() was returned without await.
After adding await, what word prints?
function fetchData() {
return Promise.reject(new Error("nope"));
}
async function demo() {
try {
return await fetchData();
} catch (error) {
return "caught";
}
}
demo().then((value) => console.log(value));It prints caught. The awaited rejection happens inside the try block, so the catch handles it and returns the string.
Type the missing keyword that makes the condition wait for the boolean.
async function renderAdminTools() {
if (checkPermission()) {
showAdminButton();
}
}async function renderAdminTools() {
if (await checkPermission()) {
showAdminButton();
}
}Use await inside the condition so the if receives the fulfilled boolean instead of the truthy promise object.
Check your understanding
7 QUESTIONSQuestion 1 of 7What does calling an async function always give back immediately?
Choose an answer to see the explanation.
Question 2 of 7What does this print?
Read the code, then predictasync function demo() { console.log("A"); await "B"; console.log("C"); } demo(); console.log("D");Choose an answer to see the explanation.
Question 3 of 7Why can
try { return await fetchData(); } catch { ... }catch a rejection whenreturn fetchData()cannot?Choose an answer to see the explanation.
Question 4 of 7What message does this rejected async call print?
Read the code, then predictasync function fail() { throw new Error("boom"); } fail().catch((error) => console.log(error.message));Choose an answer to see the explanation.
Question 5 of 7Where is top-level await valid in standard JavaScript?
Choose an answer to see the explanation.
Question 6 of 7What is a good mental model for
await value?Choose an answer to see the explanation.
Question 7 of 7What is wrong with
if (checkPermission())whencheckPermissionreturns a promise?Choose an answer to see the explanation.
Key takeaways
- Calling an async function returns a new promise immediately.
- Returns fulfill that promise, throws reject it, and promises or thenables are adopted.
awaitpauses the current async function; it does not block the whole JavaScript thread.- Even
awaiton a plain value resumes later. - Use
try/catcharound awaited expressions, and usereturn awaitwhen that local catch or finally timing matters. - Top-level await is for ES modules; classic scripts get a syntax error.
Final definition: async/await is promise syntax that lets one function pause at promise boundaries while the rest of JavaScript keeps running.
Up next: Sequential vs parallel.