Async and await in JavaScript let you write asynchronous code that reads top to bottom. An async function always hands back a promise, and await inside it pauses that function until a promise settles. The part beginners miss is that awaiting twice in a row is not the same as awaiting them side by side — one waits, the other overlaps.
What does async/await actually change?
An async function runs synchronously until it reaches its first await. At that point the function pauses and returns control to whoever called it, and the rest of the body resumes later, once the awaited promise settles. Because the function pauses rather than blocks, everything else in your program keeps running in the meantime.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function orderCoffee() {
console.log('ordering...');
await wait(300);
console.log('coffee ready');
return 'latte';
}
orderCoffee().then((drink) => console.log('drink:', drink));
console.log('this prints while we wait');
The output shows the pause clearly:
ordering...
this prints while we wait
coffee ready
drink: latte
Three things are worth noticing. The call to orderCoffee() does not wait for the coffee. The function returns a promise, which is why the drink only appears after a then call. And await is only legal inside an async function — writing it anywhere else is a syntax error the file cannot even parse.
Why does an await inside a loop run one item at a time?
Because each await pauses the whole function until it resolves. If the next await only starts after the previous one finished, the waits cannot overlap. A loop body with an await in it is the most common way people accidentally serialize work that could have run together.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function fetchRow(id) {
await wait(100);
return 'row ' + id;
}
async function oneByOne() {
const started = Date.now();
const rows = [];
for (const id of [1, 2, 3, 4]) {
rows.push(await fetchRow(id));
}
console.log('one by one took', Date.now() - started, 'ms');
return rows;
}
oneByOne();
One run of that file printed:
one by one took 447 ms
Four waits of a hundred milliseconds each took a little over four hundred milliseconds, because the second request was not even sent until the first one came back. The exact number changes between runs, but the shape of it does not: the total is roughly the sum of the waits.
How do you start several waits at the same time?
Build the list of promises first, hand them to Promise.all, and await that. All the waits are already in flight by the time you await, so the total time is the slowest single wait rather than the sum of them.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function fetchRow(id) {
await wait(100);
return 'row ' + id;
}
async function allAtOnce() {
const started = Date.now();
const rows = await Promise.all([1, 2, 3, 4].map((id) => fetchRow(id)));
console.log('all at once took', Date.now() - started, 'ms');
return rows;
}
allAtOnce();
The output for the same four rows:
all at once took 110 ms
Same work, roughly a quarter of the time. The rows come back in the order you listed them, not in the order they finished, because Promise.all keeps the input positions.
For three separate loads the difference is just as obvious. One after another:
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function loadEverythingInOrder() {
const started = Date.now();
const user = await wait(120);
const orders = await wait(180);
const settings = await wait(90);
console.log('sequential finished in', Date.now() - started, 'ms');
return { user, orders, settings };
}
loadEverythingInOrder();
And the same three loads started together:
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function loadEverythingAtOnce() {
const started = Date.now();
await Promise.all([wait(120), wait(180), wait(90)]);
console.log('parallel finished in', Date.now() - started, 'ms');
}
loadEverythingAtOnce();
Here is how the two runs compared on the same machine:
| Approach | Waits in flight at once | Observed time |
|---|---|---|
| Three awaits in a row | 1 | 415 ms |
Promise.all over the three | 3 | 196 ms |
Loop with await in the body | 1 | 447 ms |
Promise.all over a mapped array | 4 | 110 ms |
The sequential numbers sit near the sum of the waits, and the parallel numbers sit near the slowest wait. That is the whole calculation.
When is waiting one after another the right choice?
Some work genuinely depends on earlier work. A report needs the user before it can be built, and a second request needs an id that the first request returned. In that case the order is not an accident — running them together would be impossible.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function load(name) {
await wait(100);
return { name, loaded: true };
}
async function buildReport(user, settings) {
await wait(80);
return 'report for ' + user.name + ' with ' + settings.name;
}
async function dashboard() {
const started = Date.now();
const [user, settings] = await Promise.all([load('user'), load('settings')]);
const report = await buildReport(user, settings);
console.log('dashboard ready in', Date.now() - started, 'ms:', report);
}
dashboard();
The output shows the mix working as intended:
dashboard ready in 203 ms: report for user with settings
The two independent loads overlap, then the dependent report waits for both. The decision to parallelise belongs at the point where you know the dependency, not at the top of the function by habit.
Which mistakes quietly break async code?
The first one is forgetting the await. An async function returns a promise, so an un-awaited call hands you a promise object instead of the value you wanted, and any later use of that value fails in a confusing way.
The second is using an async callback with a method that ignores return values. forEach runs the callback and moves on; it never waits for it, so anything the callback does later lands after your code has already finished.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const results = [];
[1, 2, 3].forEach(async (id) => {
await wait(50);
results.push(id);
});
console.log('right after forEach, results length:', results.length);
The output is:
right after forEach, results length: 0
The array is still empty at that point, because the callbacks are still waiting. When you need to await inside a loop, use a for...of loop with await in the body, or a plain for loop, and both give you the sequential behaviour you asked for.
Two rules that cover most async code
- Every
awaitis a pause for the whole function, so the next line runs after the awaited promise settles. - Two awaits written one after the other never overlap; to overlap them, start the work first and await the promises together with
Promise.all. - Use
for...ofwithawaitwhen each step needs the previous result, andPromise.allover a mapped array when the steps are independent. - An async function always returns a promise, so the caller needs
awaitorthento get the result. - Methods like
forEachignore returned promises, so an async callback inside them finishes after your code has moved on.
Try it yourself: Write a wait(ms) helper and a function loadIds(ids) that takes an array of ids. Run it twice: once with a for...of loop that awaits each id in turn, and once with Promise.all over the ids. Print Date.now() - started in both runs with four ids of a hundred milliseconds each, and note how the two totals differ. Then change the loop version so it starts every request before awaiting any of them, and check that the two versions now cost the same.