Namraj Pudasaini
Jun 6, 2026
Asynchronous programming lets your code continue running while waiting for something slow — a database query, a network request, or a timer. Instead of blocking execution on each line, JavaScript moves on and handles the result when it arrives.
Browsers provide APIs for this: setTimeout, fetch, Promise, and more. In the backend, you will encounter REST and GraphQL APIs that work the same way.
setTimeoutA callback is a function you pass to another function to be called later. JavaScript uses callbacks everywhere — forEach, map, filter, fetch, addEventListener, and setTimeout.
setTimeout in ActionsetTimeout(() => console.log("Hello"), 1000);
setTimeout(() => console.log("World"), 2000);
setTimeout(() => console.log("Done"), 5000);
The order of execution does not match the order in the code. "Hello" prints after 1 second, "World" after 2 seconds, and "Done" after 5 seconds. JavaScript does not wait — it sets up the timers and continues.
This happens because of the event loop. Asynchronous functions are queued and only executed when their turn comes. Each browser tab, Node.js process, and Web Worker has its own event loop, so they run independently without blocking each other.
When one asynchronous operation depends on the result of another, you nest callbacks:
setTimeout(() => {
console.log("First");
setTimeout(() => {
console.log("Second");
setTimeout(() => {
console.log("Third");
}, 2000);
}, 1000);
}, 500);
This works, but deeply nested callbacks become hard to read and maintain — a problem known as callback hell. Promises solve this.
You can write your own functions that accept callbacks:
function handleName(name, cb) {
const fullName = `${name} Carter`;
cb(fullName);
}
function makeUpperCasename(value) {
console.log(value.toUpperCase());
}
function reverseName(value) {
console.log(value.split("").reverse().join(""));
}
handleName("Jimmy", makeUpperCasename);
handleName("Jimmy", reverseName);
The cb parameter is invoked inside handleName. You pass any function as the second argument — it gets called with the result.
A Promise represents a value that may not be available yet. You create one with the new constructor, which takes a function with resolve and reject callbacks:
const promise = new Promise((resolve, reject) => {
// do something async
resolve("It worked!");
// or: reject("Something went wrong");
});
Handle the result with .then() and .catch():
promise
.then((data) => console.log(data))
.catch((error) => console.error(error));
Promises chain naturally, which eliminates callback hell:
fetchUser()
.then((user) => fetchPosts(user.id))
.then((posts) => console.log(posts))
.catch((error) => console.error(error));
Each .then() receives the result of the previous one. Errors bubble to the nearest .catch().
async/await is syntactic sugar over Promises that makes asynchronous code read like synchronous code:
async function getProducts() {
const response = await fetch("/api/products");
const data = await response.json();
console.log(data);
}
getProducts();
async to use await inside it.await pauses execution until the Promise resolves.await calls in try/catch for error handling.async function getData() {
try {
const response = await fetch(url);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
fetch makes HTTP requests from the browser. It returns a Promise that resolves to a Response object:
(async () => {
try {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
})();
The Response object has a body you need to parse — .json() for JSON, .text() for plain text, .blob() for binary data. Each of these also returns a Promise, so you await them too.
setTimeout. Avoid deep nesting.All three approaches handle the same fundamental problem: running code without blocking. The difference is readability and maintainability as your code grows.