How to Use JavaScript Async Await With Examples You Can Run
Convert promise chains to JavaScript async/await, run requests in parallel with Promise.all, and catch errors with try/catch. Examples you can run.

async makes a function return a promise, no matter what it returns. await pauses that function until a promise settles, then gives you the fulfilled value or throws the rejection reason. Together they let you write promise-based code top to bottom, with a normal try/catch for errors, instead of a chain of .then() callbacks.
The three jobs most people search for are converting an existing .then() chain, running several requests at once with Promise.all, and catching errors cleanly. All three are below, with Node.js examples you can run and the mistakes that make people think await isn't waiting.
How async await works in JavaScript#
According to MDN's async function reference, calling an async function returns a new promise. That promise resolves with whatever the function returns, or rejects with any exception the function didn't catch. If you return a plain value, it's wrapped for you:
async function foo() {
return 1;
}
// behaves like
function foo() {
return Promise.resolve(1);
}
Inside the function, await suspends execution until the awaited promise is fulfilled or rejected. The fulfilled value becomes the value of the await expression. A rejection becomes a thrown exception at that line, which is why try/catch works.
Two details explain most of the confusing behavior:
- The code before the first
awaitruns synchronously. When the caller invokes the function, everything up to the firstawaitexecutes right away. Then control returns to the caller, and the rest of the function runs later, as if it were in a.then()callback. awaitis only valid inside anasyncfunction, or at the top level of a JavaScript module. Use it anywhere else and you get aSyntaxError.
The difference between the two keywords is simple. async goes on the function and changes what it returns. await goes on an expression inside that function and unwraps a promise.
The smallest example you can run#
Save this as a .js file and run it with Node.js. Watch the order of the output.
function resolveAfter(ms, value) {
return new Promise((resolve) => {
setTimeout(() => resolve(value), ms);
});
}
async function main() {
console.log("calling");
const result = await resolveAfter(200, "resolved");
console.log("got:", result);
return "done";
}
main().then((value) => console.log("main() returned:", value));
console.log("this line runs before the result arrives");
The first log inside main() runs synchronously because it sits before the first await. The log after the main() call runs next, because control has already returned to the caller. Then the timer fires, await resumes, and the remaining lines run. Note that main() returns a promise, so the caller still needs .then() or its own await to get the return value.
Convert a promise chain to async/await#
Take this chain from MDN's Using promises guide:
const listOfIngredients = [];
doSomething()
.then((url) => fetch(url))
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
})
.then(() => {
console.log(listOfIngredients);
});
Here's the same logic as an async function:
const listOfIngredients = [];
async function logIngredients() {
const url = await doSomething();
const res = await fetch(url);
const data = await res.json();
listOfIngredients.push(data);
console.log(listOfIngredients);
}
The conversion is mechanical:
- Wrap the chain in a function and mark it
async. - Each
.then((x) => ...)becomesconst x = await ...;on its own line. - A trailing
.catch(handler)becomes atry/catcharound the awaits. - The final value you'd pass down the chain becomes a
return.
doSomething() and fetch() don't change at all. They still return promises. That's the main reason this refactor is cheap: async/await builds on promises rather than replacing them, so you can convert one function at a time.
You can also keep a .catch() inline when the fallback is a one-liner. These two versions are equivalent:
async function getProcessedData(url) {
let v;
try {
v = await downloadData(url);
} catch (e) {
v = await downloadFallbackData(url);
}
return processDataInWorker(v);
}
async function getProcessedData(url) {
const v = await downloadData(url).catch((e) => downloadFallbackData(url));
return processDataInWorker(v);
}
Notice there's no await before the return. The promise from processDataInWorker is adopted by the async function's own promise, so return await isn't needed here, although it would be valid too.
Fetch with async/await#
fetch() is where most people meet async/await for the first time. It returns a promise that resolves to a Response object, and reading the body is a second promise, so you need two awaits:
async function loadUser(id) {
const res = await fetch(`/api/users/${id}`);
const data = await res.json();
return data;
}
// inside another async function:
const user = await loadUser(42);
console.log(user);
If you call loadUser(42) from non-async code, remember you're holding a promise, not the user object. Either await it inside an async function or attach .then().
Status checks, headers, request bodies and what counts as a fetch error deserve their own treatment. The CoreMesh article on using the JavaScript Fetch API with examples you can run covers that, and if the browser console blocks your request, see which header is missing when you hit a CORS error.
Run requests in parallel with Promise.all#
The most common performance mistake with async/await is awaiting things one at a time when they don't depend on each other. This version waits for the first request to finish before the second one even starts:
async function sequential() {
const a = await fetchA(); // fetchB() hasn't been called yet
const b = await fetchB();
return [a, b];
}
With Promise.all, both start immediately and you wait once for both:
async function concurrent() {
const [a, b] = await Promise.all([fetchA(), fetchB()]);
return [a, b];
}
Run this to see the difference in elapsed time:
const wait = (ms, value) =>
new Promise((resolve) => setTimeout(() => resolve(value), ms));
async function sequential() {
const start = Date.now();
const slow = await wait(300, "slow");
const fast = await wait(200, "fast");
console.log("sequential:", slow, fast, `${Date.now() - start}ms`);
}
async function concurrent() {
const start = Date.now();
const [slow, fast] = await Promise.all([wait(300, "slow"), wait(200, "fast")]);
console.log("concurrent:", slow, fast, `${Date.now() - start}ms`);
}
sequential().then(concurrent);
For a list of items, map to promises first, then await the whole array:
const users = await Promise.all(ids.map((id) => loadUser(id)));
Two rules from MDN's reference deserve strict attention:
- Don't write
[await p1, await p2]as a shortcut. It looks concurrent, but ifp2rejects beforep1settles, that rejection is unhandled, even if the caller has a.catch().Promise.allwires everything up at once, so a rejection always surfaces in the normal chain. Promise.allfails fast. If one promise rejects,Promise.allrejects immediately. The other operations keep running, but you lose access to their results. When you need every outcome, success or failure, usePromise.allSettled()instead.Promise.any()andPromise.race()round out the set.
Handle errors with try/catch#
A rejected promise becomes a thrown exception at the await, so one try/catch covers every step inside it, exactly like synchronous code:
async function foo() {
try {
const result = await doSomething();
const newResult = await doSomethingElse(result);
const finalResult = await doThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch (error) {
failureCallback(error);
}
}
Code after the catch block still runs, which replaces the .catch().then() pattern. Nested try blocks let you make a step optional without swallowing errors from the critical steps around it:
async function main() {
try {
const result = await doSomethingCritical();
try {
const optionalResult = await doSomethingOptional(result);
await doSomethingExtraNice(optionalResult);
} catch (e) {
// Ignore failures in optional steps and proceed.
}
await moreCriticalStuff();
} catch (e) {
console.error(`Critical failure: ${e.message}`);
}
}
Here's a runnable version with a primary source that fails and a fallback that succeeds:
const fail = (ms, message) =>
new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms));
const ok = (ms, value) =>
new Promise((resolve) => setTimeout(() => resolve(value), ms));
async function loadWithFallback() {
let data;
try {
data = await fail(100, "primary source down");
} catch (e) {
console.log("caught:", e.message);
data = await ok(100, "fallback data");
}
return data.toUpperCase();
}
loadWithFallback()
.then((value) => console.log("result:", value))
.catch((e) => console.log("outer catch:", e.message));
One thing try/catch can't do for you is catch errors from an async function nobody awaits. If loadWithFallback() threw after its own catch, say inside toUpperCase(), the error would reject the returned promise. The .catch() on the last line handles that. Without it, or without an await inside an outer try, you get an unhandled rejection.
Async/await vs promises#
They're the same mechanism. MDN states that async/await has the same concurrency semantics as a normal promise chain, and that an await doesn't stop the whole program, only the parts that depend on its value. So the choice is about readability, not capability.
| Situation | What I'd use |
|---|---|
| Several steps that each depend on the previous result | async/await |
| Error handling that spans several steps | async/await with try/catch |
A single transform, like .then((res) => res.json()) |
Either; .then() is fine |
| Inline fallback on one call | await x().catch(fallback) |
| Several independent operations | Promise.all inside an async function |
| Running a list of async steps in order | for...of with await |
The sequential loop MDN shows is a good illustration of how much clearer the async form is:
let result;
for (const f of [func1, func2, func3]) {
result = await f(result);
}
The one real tradeoff MDN calls out is that it's easy to forget await, and nothing warns you until you try to use a promise as a value. That leads straight into the gotchas.
Common mistakes: why await isn't waiting#
Most "await not working" questions come down to one of these.
You forgot the await. The variable holds a pending promise, and logging it shows a Promise object rather than your data. Add await, and make sure the enclosing function is async.
The await is in a callback that isn't async. This is a SyntaxError:
async function loadAll(ids) {
ids.forEach((id) => {
const user = await loadUser(id); // SyntaxError: await is only valid in async functions
});
}
Making the callback async fixes the syntax error but introduces a new problem. Each call to the async callback returns a promise, and forEach ignores return values, so loadAll finishes before any of the loads do. Use for...of with await for sequential work, or Promise.all(ids.map(...)) for concurrent work.
The caller doesn't await the async function. The function itself works, but the caller carries on immediately. Every async function returns a promise, and that promise has to be awaited or handled with .then() somewhere up the call stack.
You put a line break between async and function. Automatic semicolon insertion turns async into a plain identifier, and the rest becomes an ordinary function declaration. Keep them on the same line.
You awaited sequentially when you meant parallel. See the Promise.all section above. Starting two promises first and awaiting them afterwards does run them concurrently, but it has the unhandled-rejection problem described there. Prefer Promise.all.
You're using await at the top level of a script. Top-level await only works in JavaScript modules. In a plain script, wrap the code in an anonymous async function and call it right away:
(async () => {
const data = await loadUser(1);
console.log(data);
})();
This immediately invoked async arrow function is the standard way to get an await context in a non-module file.
You compared promise identity. Returning a promise from an async function gives you a new promise, not the one you returned, so p === asyncFn() is false even when asyncFn does return p. It rarely matters, but it surprises people writing tests.
If you also work in Python, the mental model carries over almost directly. The CoreMesh article on Python async await with examples you can run covers the equivalent of Promise.all there.
Quick reference#
| Promise form | async/await form |
|---|---|
fn().then((v) => use(v)) |
const v = await fn(); use(v); |
fn().catch(handle) |
try { await fn(); } catch (e) { handle(e); } |
fn().then(a).then(b) |
const x = await fn(); const y = await a(x); await b(y); |
.catch(h).then(next) |
try { ... } catch (e) { h(e); } next(); |
Promise.all([a(), b()]).then(([x, y]) => ...) |
const [x, y] = await Promise.all([a(), b()]); |
function f() { return Promise.resolve(1); } |
async function f() { return 1; } |
Before you ship an async function, check:
- Every call to a promise-returning function has
awaitin front of it, or you deliberately kept the promise. awaitonly appears insideasyncfunctions or at a module's top level.- Independent operations go through
Promise.allorPromise.allSettled, not oneawaitafter another. - Errors are caught with
try/catchinside the function, or the caller awaits the function inside its owntry. - No
asynccallback is passed toforEach. - No async function is called without its promise being awaited or handled somewhere.
Quick answers
What is the difference between async and await?
async goes on a function declaration and makes that function always return a promise. await goes on an expression inside an async function and pauses the function until that promise settles, giving you the value or throwing the rejection.
Is async/await faster than promises?
No. MDN's Using promises guide says async/await has the same concurrency semantics as a promise chain. Speed comes from starting independent work at the same time with Promise.all, which you can do in either style.
Why is my await not waiting?
Usually the await is missing, the enclosing function isn't marked async, or the caller doesn't await the async function's returned promise. An async callback passed to forEach is another common cause, since forEach ignores the promises it returns.
Can I use await outside an async function?
Only at the top level of a JavaScript module. Anywhere else it's a SyntaxError, so wrap the code in an async function and call it.
Does await block the whole program?
No. It suspends only the async function it's in. Other code and other pending promises keep running while the await is pending.
Did this solve it?
One tap. No account.
Sorry about that. What was missing?
from 0 readers
Saved you an hour? Send a tip.
USDC or ETH on Base, Ethereum, Arbitrum and more. Keeps the notes free and the examples running.
Network
0xc783bc5c3fa14cEC3f09A22284bC91C5AF0d59e1
On a computer? Scan the code with your phone wallet, or copy the address.
Pick the network your wallet or exchange uses. USDC works on all six; ETH on Base, Ethereum, Arbitrum and Optimism. Don't send on Tron or Solana: this address doesn't exist there. Tips are optional and unlock nothing; every guide stays free.
Discussion
Start the discussion
Next note