What is Hoisting?
Hoisting is a default behavior in JavaScript where declarations such as variables, functions, and classes are moved to the top of their scope during compilation before execution.
But their behavior differs:
What exactly gets hoisted?
1. Variable declarations
-
var→ hoisted and initialized withundefinedHere:- variable is hoisted
- memory allocated
- initialized with undefined
-
let, const, class→ hoisted but Inaccessible before declaration (Temporal Dead Zone)
Initialized: Memory is allocated and given an initial value
Inaccessible: Memory exists, but JavaScript does not allow access yet
2. Function declarations
- Fully hoisted (you can call them before declaration)
3. Class declarations
- Hoisted but behave like
let/const(TDZ applies)
Important: Only the declaration is hoisted, not the initialization
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();