JavaScript Intermediate #2 Async Intro — Promise and async/await
After #1 Classes, this post covers one of JavaScript’s biggest features — async. Tough on first contact — but once you’re used to it, JavaScript’s real expressiveness opens up.
Sync vs async — what’s the difference? #
JavaScript executes line by line, by default. Top to bottom.
console.log('1');
console.log('2');
console.log('3');
// 1, 2, 3 (in this order)
The problem is time-consuming work (file reads, network requests, timers, etc.). If JavaScript waited synchronously, everything else would freeze. So JavaScript handles those asynchronously — in the form of “tell me when it’s done.”
console.log('1');
setTimeout(() => console.log('2'), 1000);
console.log('3');
// 1, 3, 2 (in this order)
setTimeout registers a function to run in 1 second and the body keeps going. console.log('3') runs first.
The age of callbacks — and its limits #
Old JavaScript handled async with callback functions.
fetchUser(userId, (user) => {
fetchPosts(user.id, (posts) => {
fetchComments(posts[0].id, (comments) => {
console.log(comments);
});
});
});The famous callback hell. Indentation grows deeper, and error handling spreads across every level.
fetchUser(userId, (err, user) => {
if (err) return handleError(err);
fetchPosts(user.id, (err, posts) => {
if (err) return handleError(err);
fetchComments(posts[0].id, (err, comments) => {
if (err) return handleError(err);
console.log(comments);
});
});
});ES2015’s Promise solved this.
Promise — an object that represents an async result #
A Promise represents “a value that doesn’t exist yet but will.” It holds one of three states.
- pending — not finished yet
- fulfilled — succeeded with a value
- rejected — errored
Creating a Promise — new Promise
#
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('success');
} else {
reject(new Error('failure'));
}
}, 1000);
});Receives resolve and reject functions. When the async work finishes, call one of them.
Creating manually isn’t actually common. Most async APIs (fetch, fs.promises, timers) already return Promises.
.then / .catch / .finally
#
Methods for handling a Promise’s result.
promise
.then((value) => console.log('success:', value))
.catch((err) => console.error('failure:', err))
.finally(() => console.log('done'));.then(callback)— on success.catch(callback)— on failure.finally(callback)— at the end regardless
Chain — pass results forward #
What .then’s callback returns becomes the input to the next .then. If the callback returns another Promise, its result is unwrapped for the next.
fetchUser(userId)
.then((user) => fetchPosts(user.id)) // takes user, returns a posts Promise
.then((posts) => fetchComments(posts[0].id)) // takes posts, returns a comments Promise
.then((comments) => console.log(comments))
.catch((err) => handleError(err));Callback hell flattened out. Error handling collected in one final .catch.
async / await — straightening Promises #
Syntax added in ES2017. Layered on top of Promises so you can write as if it’s synchronous code.
async function loadComments(userId) {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
return comments;
}
loadComments(userId)
.then((comments) => console.log(comments))
.catch((err) => handleError(err));Rules:
- Putting
asyncin front of a function means the function always returns a Promise - You can use
awaitonly inside anasyncfunction —awaitunwraps a Promise and returns the value
These two are key. Hitting await, JavaScript pauses briefly at that line to wait for the result, then continues to the next line (other code can run during the wait).
Errors via try/catch #
async function loadComments(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
return comments;
} catch (err) {
handleError(err);
throw err;
}
}Just like sync code, you catch every async error in one place with try/catch. One of async/await’s biggest benefits.
Parallel execution — Promise.all
#
A pitfall in the code above — three fetches run sequentially (the second starts after the first ends). Where they could run together, that’s wasted time.
For independent async, run them in parallel with Promise.all.
async function loadDashboard(userId) {
const [user, notifications, settings] = await Promise.all([
fetchUser(userId),
fetchNotifications(userId),
fetchSettings(userId),
]);
return { user, notifications, settings };
}Three requests start at once, and when all finish you get the results in an array. Receive via destructuring (#5 Objects and Arrays).
Pitfall of Promise.all — one failure fails all
#
const results = await Promise.all([
fetchA(), // success
fetchB(), // failure
fetchC(), // success
]);
// the whole thing rejects — fetchA, fetchC results are not received
If that’s a burden, use Promise.allSettled.
const results = await Promise.allSettled([
fetchA(),
fetchB(),
fetchC(),
]);
// results = [
// { status: 'fulfilled', value: ... },
// { status: 'rejected', reason: ... },
// { status: 'fulfilled', value: ... },
// ]
Each result comes as an object — handle success/failure individually.
Other commonly used Promise static methods #
// fastest result (success or failure)
const fastest = await Promise.race([fetchA(), fetchB()]);
// fastest success only
const firstSuccess = await Promise.any([fetchA(), fetchB(), fetchC()]);
// immediately resolved/rejected
Promise.resolve(42); // immediately fulfilled
Promise.reject(new Error('!')); // immediately rejected
Promise.race is used often in timeout patterns.
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), ms)
);
}
const result = await Promise.race([
fetch('/api/slow'),
timeout(5000),
]);If fetch doesn’t finish in 5 seconds, timeout rejects first and race exits. Modern JavaScript can use AbortController together — covered in #6 fetch API.
Where to put await — pitfalls and guides
#
1) await pauses the current line briefly
#
// sequential — adds up
const a = await fetchA();
const b = await fetchB();
// parallel — faster of the two
const [a, b] = await Promise.all([fetchA(), fetchB()]);The reason loadComments was sequential — posts needed user, and comments needed posts, so there were dependencies. Where there’s no dependency, always Promise.all is faster.
2) Beware await in loops
#
for (const id of userIds) {
const user = await fetchUser(id);
console.log(user);
}For 100 users this is 100 sequential fetches — very slow. If parallel is fine:
const users = await Promise.all(
userIds.map((id) => fetchUser(id))
);Almost 100x faster.
3) Don’t forget await
#
async function process() {
const result = fetchData(); // ✗ the Promise object itself
console.log(result); // [object Promise]
console.log(result.data); // undefined
}Forgetting await makes result the Promise object itself. The compiler doesn’t catch it — be mindful (TypeScript + ESLint help).
Top-level await — ES2022 #
In ES Modules, you can use await outside functions.
// app.js (ES module)
const data = await fetch('/api/init').then((r) => r.json());
console.log(data);Doesn’t work in CommonJS modules. Only modern ESM. Available in modern tools like Vite, Next.js, and Bun.
Wrap-up #
What we covered:
- JavaScript runs line by line, but slow work is async
- Async handling evolved from callback → Promise → async/await
- Three Promise states — pending / fulfilled / rejected
.then/.catch/.finallyand chainasyncfunctions return Promises,awaitunwraps results- Run independent async in parallel with
Promise.all Promise.allSettled/race/any- Beware
awaitin loops; don’t forgetawait - Top-level await in ESM
In the next post (#3 Iterators and Generators) we cover the contract for...of follows, building your own iterables, and using generators to express lazy sequences.