JavaScript Async Await: A Beginner’s Guide With Examples
Async await lets you write code that waits for slow things, like network calls, without callback chaos. Learn how it works with examples we ran, plus the mistakes that slow your app down.
- An
asyncfunction always returns a Promise, andawaitpauses only that function until the Promise settles. - Use
try/catcharoundawaitto handle rejected Promises. - Independent tasks should run together with
Promise.all. In our test, three 200 ms calls took about 600 ms one after another and about 200 ms together. awaitworks only inside async functions and at the top level of ES modules.
JavaScript is single-threaded, but a page still needs to download data, read files and wait for timers without freezing. Async await is the modern syntax for that waiting, and it is built on Promises.
This guide uses examples we ran in Node.js. If you are new to the language, start with our JavaScript tutorial for beginners.
Why asynchronous code exists
A network request can take hundreds of milliseconds. If JavaScript stopped everything while it waited, buttons would stop responding. So slow operations return a Promise, an object that stands for a value that will arrive later, and your code continues.
Older code chained .then() calls. Async await lets you write the same logic top to bottom, like normal code, which is easier to read and debug.
Async and await basics
const wait = (ms, v) => new Promise(r => setTimeout(() => r(v), ms));
async function getUser(id) {
await wait(200); // pretend this is a network call
return { id, name: "User " + id };
}
async function main() {
const user = await getUser(1);
console.log(user); // { id: 1, name: 'User 1' }
}
main();
Putting async before a function makes it return a Promise. Putting await before a Promise pauses that function until the Promise has a value. The rest of the program keeps running during the pause.
The forgotten await
The most common beginner bug is forgetting await. You then hold the Promise itself, not its result.
console.log("Without await:", getUser(2));
// Without await: Promise { <pending> }
If you see Promise { <pending> } where you expected data, check for a missing await.
Error handling with try/catch
When a Promise rejects, await throws the error inside your async function, so ordinary try/catch works.
async function failing() {
await wait(10);
throw new Error("Server said 500");
}
try {
await failing();
} catch (e) {
console.log("caught:", e.message); // caught: Server said 500
}
Without the try/catch, the rejection becomes an unhandled error, which can crash a Node.js process. When you test APIs that fail on purpose, our guide on HTTP status codes helps you pick the right cases.
Sequential vs parallel: the speed trap
Each await waits before the next line runs. If your calls do not depend on each other, that wastes time.
// Sequential: about 600 ms
const a = await getUser(1);
const b = await getUser(2);
const c = await getUser(3);
// Parallel: about 200 ms
const all = await Promise.all([getUser(1), getUser(2), getUser(3)]);
console.log(all.map(x => x.name)); // [ 'User 1', 'User 2', 'User 3' ]
The timings come from our own run, where each fake call takes 200 ms. Starting all three at once and waiting together cut the time to a third.
| Helper | Resolves when | If one fails |
|---|---|---|
Promise.all |
All succeed | Rejects immediately |
Promise.allSettled |
All finish | Gives each result as fulfilled or rejected |
Promise.race |
The first one finishes | Follows the first result |
Promise.any |
The first one succeeds | Rejects only if all fail |
const res = await Promise.allSettled([getUser(1), failing()]);
console.log(res.map(r => r.status)); // [ 'fulfilled', 'rejected' ]
Async in loops and map
An async callback returns a Promise, so map gives you an array of Promises, not values.
console.log([1, 2, 3].map(async n => n * 2));
// [ Promise { 2 }, Promise { 4 }, Promise { 6 } ]
const doubled = await Promise.all([1, 2, 3].map(async n => n * 2));
console.log(doubled); // [ 2, 4, 6 ]
Wrap the result in Promise.all. Avoid forEach with await inside, because forEach does not wait for the callbacks. Use a for...of loop when you really need steps in order.
A real fetch example
async function loadUser(id) {
const res = await fetch(`https://api.example.com/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
fetch rejects only on network failure. A 404 or 500 still resolves, so check res.ok yourself. TypeScript can describe the returned shape for you, see our TypeScript tutorial for beginners.
Tip: add a timeout to long requests with AbortSignal.timeout(5000) passed to fetch, so one slow server cannot hang your feature.
Final thoughts
Learn three habits: always await a Promise you need, wrap risky awaits in try/catch and run independent work with Promise.all. Those cover most real async code.
Frequently asked questions
What does async await do in JavaScript?
It lets you write asynchronous code in a readable, top-to-bottom style. An async function returns a Promise, and await pauses it until another Promise gives a result.
Is async await better than promises?
It is built on promises, not a replacement. It usually reads more clearly, but you still use promise helpers such as Promise.all when tasks run in parallel.
Can I use await outside an async function?
Only at the top level of an ES module. In older scripts and inside normal functions, await is a syntax error.
How do I run async tasks in parallel?
Start them without awaiting each one and wait for them together with Promise.all. This is faster than awaiting them one after another.
Why does my async function return Promise pending?
You probably forgot await. An async function always returns a Promise, so you must await it, or call then, to get the value.
Keep learning
Need help with testing, WordPress or SEO?
We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.