Asynchronous JavaScript matters once your pages get past the basics. Callbacks, promises, and async/await – for work that takes time (API calls, timers, file reads) without freezing the page.
What is asynchronous programming?
Asynchronous code lets JavaScript start a slow task and carry on with other work while it waits. The page stays responsive instead of blocking until the task finishes.
Callbacks
A callback is a function passed to another function to be called later. This was the original way to handle async work in JavaScript.
Example – using a callback
function fetchData(callback) {
setTimeout(() => {
callback("Here's your data!");
}, 2000);
}
fetchData(data => {
console.log(data); // Logs "Here's your data!" after 2 seconds
});
Callbacks work, but nested callbacks get hard to read – often called ‘callback hell’.
Promises
A Promise represents a value that will arrive later – or an error if something goes wrong. It has three states: pending, resolved, or rejected.
Creating a promise
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data fetched successfully");
}, 2000);
});
}
Consuming a promise
fetchData().then(data => {
console.log(data); // Logs "Data fetched successfully" after 2 seconds
}).catch(error => {
console.error(error);
});
Promises flatten nested callbacks into a chain of .then() calls.
Async/await
Async/await (ES2017) is syntax on top of promises. It makes asynchronous code read more like normal sequential code.
Async/await example
async function fetchAndDisplayData() {
try {
let data = await fetchData();
console.log(data); // Logs "Data fetched successfully" after 2 seconds
} catch (error) {
console.error(error);
}
}
fetchAndDisplayData();
async marks a function as asynchronous. await pauses until a promise resolves.
Handling multiple asynchronous operations
When you need several async tasks at once, Promise.all() runs them in parallel.
Example – Promise.all()
Promise.all([fetchData1(), fetchData2(), fetchData3()])
.then(([data1, data2, data3]) => {
console.log(data1, data2, data3);
})
.catch(error => {
console.error(error);
});
All three promises run at the same time. The results arrive in an array.
Error handling
Always handle errors in async code. Use .catch() with promises, or try...catch with async/await.
Example – error handling with async/await
async function fetchSafeData() {
try {
let data = await fetchData();
console.log(data);
} catch (error) {
console.error("Failed to fetch data:", error);
}
}
fetchSafeData();
Best practices
- Avoid callback hell: prefer promises or async/await over deeply nested callbacks.
- Error handling: always catch failures in async code.
- Keep it readable: if a chain gets long, break it into named functions.
- Parallel operations: use
Promise.all()when tasks do not depend on each other.
Most real JavaScript apps rely on async code. Learn all three patterns – you will still see callbacks in older code, promises everywhere, and async/await in newer projects.

