The complete evolution of async JavaScript — callbacks, Promises, and async/await — with practical patterns and error handling techniques.
JavaScript runs on a single thread. If a network request blocked the thread, the entire UI would freeze. Async programming lets JavaScript start a task and continue running, picking up the result when it's ready.
The original async pattern — pass a function to be called when the work is done.
fs.readFile('data.json', 'utf8', (err, data) => {
if (err) return console.error(err);
const parsed = JSON.parse(data);
db.save(parsed, (err, result) => {
if (err) return console.error(err);
emailService.send(result.id, (err) => {
// Callback hell: 🔥
});
});
});
Callbacks work but lead to deeply nested, hard-to-read code — known as callback hell.
A Promise is an object representing a future value — either resolved (success) or rejected (failure).
fetch('/api/users/1')
.then(res => res.json())
.then(user => db.save(user))
.then(result => emailService.send(result.id))
.catch(err => console.error('Something failed:', err));
Creating your own Promise:
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
delay(1000).then(() => console.log('1 second passed'));
Async/await is syntactic sugar over Promises — it makes async code look synchronous.
async function processUser(userId) {
const res = await fetch(`/api/users/${userId}`);
const user = await res.json();
const saved = await db.save(user);
await emailService.send(saved.id);
return saved;
}
// try/catch with async/await
async function getUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error('Failed to fetch user:', err);
return null;
}
}
// Utility: wrap async to avoid try/catch boilerplate
async function to(promise) {
try {
return [null, await promise];
} catch (err) {
return [err, null];
}
}
const [err, user] = await to(getUser(1));
if (err) { /* handle */ }
// Sequential — slow (waits for each)
const user = await getUser(1);
const posts = await getPosts(1);
// Parallel — fast (runs concurrently)
const [user, posts] = await Promise.all([getUser(1), getPosts(1)]);
// Race — first one wins
const result = await Promise.race([fetchFromPrimary(), fetchFromFallback()]);
// All settle (don't throw on partial failure)
const results = await Promise.allSettled([api1(), api2(), api3()]);
results.forEach(r => r.status === 'fulfilled' ? use(r.value) : log(r.reason));