What is Asynchronous JavaScript?
JavaScript is a synchronous single-threaded language by default and executes code line-by-line (synchronously). but asynchronous behavior is achieved
By default JavaScript has Blocking Behavior means waits for the current operation to complete before moving to the next line.
Non-Blocking Behavior
JavaScript can continue executing other code while waiting for a asynchronous operations to complete.
Modern JavaScript primarily uses three methods to handle asynchronous operations.
This guide walks you through callbacks, Promises, and async/await with clean, copyable examples. By the end, you'll understand the event loop and write production‑ready async code.
1. Callbacks — the foundation
Callbacks are the original foundation of asynchronous JavaScript.
Callbacks are functions passed as arguments to be executed later.
Why is it Called “Callback”?
Because:
The function is “called back” later after some tasks completes.
console.log("Start");
setTimeout(() => {
console.log("Inside callback (after 2 seconds)");
}, 2000);
console.log("End");
// Output order: Start → End → Inside callback
Why Callbacks Became So Popular
Because:
Callbacks enabled JavaScript to handle:
- Timers
- API requests
- User events
- File operations
- Database queries
But there was a problem with callback
"Callback Hell" happens when multiple asynchronous callbacks are nested inside each other, making code difficult to read, debug, and maintain.
They work well for simple tasks but can lead to "callback hell".
Callback Hell made asynchronous JavaScript code difficult to read, manage, and debug. To solve this problem, Promises were introduced in JavaScript to handle asynchronous operations in a cleaner and more structured way.
2. Promises — chainable future values
Promises represent an eventual completion (or failure). They avoid nesting and improve readability.
const fetchData = new Promise((resolve, reject) => {
setTimeout(() => resolve("Data loaded"), 1500);
});
fetchData
.then(result => console.log(result))
.catch(err => console.error(err));
// Output after 1.5s: Data loaded
3. Async/Await — the modern standard
async functions return a Promise, and await pauses execution until resolution — synchronous feel, async behaviour.
async function getUser() {
try {
const res = await fetch('https://jsonplaceholder.typicode.com/users/1');
const user = await res.json();
console.log(`${user.name}`);
} catch (err) {
console.error("Fetch failed", err);
}
}
getUser();
⚡ Understanding the event loop
Key insight: Microtasks (Promises) run before macrotasks (setTimeout). The event loop prioritizes microtask queue.
console.log("Start");
setTimeout(() => console.log("Timeout"), 0);
Promise.resolve().then(() => console.log("Promise"));
console.log("End");
// Output: 1️⃣️ Start → 4️⃣️ End → 3️⃣️ Promise → 2️⃣️ Timeout
When to use each pattern
- Callbacks: Simple event handlers, Node.js legacy APIs.
-
Promises: Concurrent requests (
Promise.all), chaining async operations. - Async/Await: Sequential logic, clean error handling, modern codebases.
async function fetchAllUsers() {
const ids = [1,2,3];
const promises = ids.map(id => fetch(`https://jsonplaceholder.typicode.com/users/${id}`).then(r => r.json()));
const users = await Promise.all(promises);
console.log(users.map(u => u.name));
}
fetchAllUsers();