Write asynchronous code that reads like ordinary code, with async functions, await, try-catch error handling and parallel work.

Bishal Biswas
Self Learner
Promises, but easier to read
async and await are a nicer way to write promises. Nothing new happens underneath, the syntax is just far closer to how you think.
function getUser() {
return fetchUser(1)
.then(user => fetchPosts(user.id))
.then(posts => posts.length)
.catch(error => console.log(error.message));
}
async function getUserAsync() {
try {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id);
return posts.length;
} catch (error) {
console.log(error.message);
}
}
The second version reads top to bottom, like normal code.
async
Put async in front of a function and two things happen:
- It always returns a promise.
- You may use
awaitinside it.
async function greet() {
return 'Hello';
}
console.log(greet());
greet().then(value => console.log(value));
Returning 'Hello' gives you a promise that resolves to 'Hello'. Throwing gives you a rejected promise:
async function fail() {
throw new Error('Something broke');
}
fail().catch(error => console.log('Caught:', error.message));
await
await pauses the function until the promise settles, then hands you the value.
async function loadUser() {
console.log('Starting');
const user = await fetchUser(1);
console.log('Got:', user.name);
return user;
}
loadUser();
console.log('This runs before the user arrives');
Important: await pauses only that function. The rest of your program keeps running, so the page never freezes.
Error handling with try-catch
This is a big win. Async errors become ordinary errors.
async function loadUser(id) {
try {
const user = await fetchUser(id);
console.log('Got:', user.name);
return user;
} catch (error) {
console.log('Failed:', error.message);
return null;
} finally {
console.log('Finished attempting');
}
}
loadUser(1);
loadUser(-1);
You can also mix styles, which is handy for a single call:
async function load() {
const user = await fetchUser(1).catch(() => null);
if (!user) {
console.log('Could not load');
return;
}
console.log(user.name);
}
The performance trap
This is the most common mistake with await.
async function loadAllSlow() {
const a = await fetchUser(1);
const b = await fetchUser(2);
const c = await fetchUser(3);
return [a, b, c];
}
Three requests, one after another. If each takes a second, this takes three.
None of them depend on each other, so run them together:
async function loadAllFast() {
const [a, b, c] = await Promise.all([
fetchUser(1),
fetchUser(2),
fetchUser(3)
]);
return [a, b, c];
}
One second instead of three.
The rule: only
awaitin sequence when a step genuinely needs the result of the one before it.
Awaiting in a loop
The same trap appears in loops:
async function loadEach(ids) {
const users = [];
for (const id of ids) {
users.push(await fetchUser(id));
}
return users;
}
async function loadTogether(ids) {
return Promise.all(ids.map(id => fetchUser(id)));
}
Sequential loops are sometimes what you want, for example when you must not overload a server. Just make it a deliberate choice.
Watch out for this one:
async function broken(ids) {
ids.forEach(async (id) => {
const user = await fetchUser(id);
console.log(user.name);
});
console.log('Done');
}
Done prints immediately. forEach does not wait for async callbacks. Use for...of or Promise.all with map.
Top level await
Inside an ES module you can use await at the top level.
const response = await fetch('/api/config');
const config = await response.json();
console.log(config);
In a classic script you cannot, so wrap it in an async IIFE:
(async () => {
const response = await fetch('/api/config');
console.log(await response.json());
})();
Awaiting a non promise
async function test() {
const value = await 42;
console.log(value);
}
test();
This works. A non promise is wrapped automatically. Handy when a function might return either.
A complete example
async function loadDashboard(userId) {
try {
const user = await fetchUser(userId);
const [posts, notifications] = await Promise.all([
fetchPosts(user.id),
fetchNotifications(user.id)
]);
return {
name: user.name,
postCount: posts.length,
unread: notifications.filter(n => !n.read).length
};
} catch (error) {
console.log('Dashboard failed:', error.message);
return null;
}
}
The user is needed first, so that one is sequential. Posts and notifications are independent, so they run together. That is the pattern you want.
Quick recap
asyncmakes a function return a promise and allowsawaitinside.awaitpauses that function only, not the whole program.- Handle errors with ordinary
try-catch. - Sequential
awaitcalls run one after another, which is often needlessly slow. - Use
Promise.allfor independent work. forEachignores async callbacks, so usefor...oformapwithPromise.all.- Top level
awaitworks in modules, not in classic scripts.
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id <= 0) reject(new Error('Invalid id: ' + id));
else resolve({ id: id, name: 'User ' + id });
}, 500);
});
}
// async always returns a promise, await unwraps one
async function loadUser(id) {
try {
const user = await fetchUser(id);
console.log('Got:', user.name);
return user;
} catch (error) {
console.log('Failed:', error.message); // ordinary try/catch works
return null;
} finally {
console.log('Finished attempting id', id);
}
}
loadUser(1);
loadUser(-1);
// SLOW: three requests one after another, about 1.5 seconds
async function loadAllSlow() {
const a = await fetchUser(1);
const b = await fetchUser(2);
const c = await fetchUser(3);
return [a, b, c];
}
// FAST: the same three requests together, about 0.5 seconds
async function loadAllFast() {
return Promise.all([fetchUser(1), fetchUser(2), fetchUser(3)]);
}
loadAllFast().then(users => console.log(users.map(u => u.name)));
// forEach does NOT wait for async callbacks
async function broken(ids) {
ids.forEach(async id => { await fetchUser(id); });
console.log('broken: this prints immediately');
}
// for...of does wait, one at a time
async function sequential(ids) {
for (const id of ids) {
const user = await fetchUser(id);
console.log('sequential:', user.name);
}
}
sequential([1, 2]);
// The real world pattern: sequential where needed, parallel where possible
async function loadDashboard(userId) {
const user = await fetchUser(userId); // needed first
const [a, b] = await Promise.all([ // independent, so together
fetchUser(user.id + 1),
fetchUser(user.id + 2)
]);
return { name: user.name, related: [a.name, b.name] };
}
loadDashboard(1).then(result => console.log(result));
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready